それはただ美しかった。
コンポーネントという呪縛がある一方で、開発者の自由さを守りながら宣言的UIによる美しい構成が可能となった。
数千行に渡るDOMイベント管理の苦行から開放された。
見上げればそこにはReactがあった。
まるでオリオン座のように。
だがしかしBUT
仮想DOMというバーチャルな世界を実現するために、「Hello World」を表示するには現実的にWebpackやRollup等のバンドルが必要になった。
これを私は「コンポーネントの呪縛」と勝手に思っている。(悪気はない。)
初学者だった頃のjQuery使いの私にはしばらく難しかった。
とりあえずこれのようなminimalなスターターを書いて、そっと公式ドキュメントを閉じた。
数年の月日を経て
複雑なステート管理にはReduxが有名に、事実上のデファクトとなった。
やがてHooksが誕生した。
Hooksの登場により再度公式ドキュメントを開き、そしてそれをそっと閉じた。
どうしてもReduxのような管理方法が必要になった
JSerとしての経験値が上がるにつれて、jQuery思想の自分にはやがて限界が訪れた。
コンポーネントである以上基本は親が子を教育するのである。
したがってグローバルなstoreへどのコンポーネントからもアクセスできる機構が必要になったのだ。
dispatchめ。(悪気はない。)
おや...
結局グローバルstoreは親から子へ移譲しているだけではないか。
親から子へデータを渡している?
それは究極の発想ではview template engineに近しいのではなかろうか。
JSならejsやhandlebars、PHPあたりではbladeとかそういった、あの{{ data }}ではなかろうか。
あの古き良き、サーバーから受け取ったデータをビューへ渡す方法ではなかろうか。
Reactを view template engine にできないだろうか
調べたらあった。
サーバーからデータを渡す:
// views/index.jsxをレンダリング
res.render('index', { message: 'Hello World' });
するとviews/index.jsx
のpropsにデータが伝搬される:
var React = require('react');
function IndexPage(props) {
return <div>{props.message}</div>;
}
module.exports = IndexPage;
素晴らしいではないか。
と思ったら
It renders static markup and does not support mounting those views on the client.
静的マークアップをレンダリングするだけでReactはマウントされないだと。。。
Reactにする意味はあるのか?(ビューの数千行が数百行くらいにはなるかもしれないが。。。)
JSのイベントはどうする?
CSS-in-JSはどうなる?
どうしてもReactをマウントしたかった
$ npm install --save @react-ssr/express
{
"scripts": {
"start": "node server.js"
}
}
const express = require('express');
const register = require('@react-ssr/express/register');
const app = express();
(async () => {
// Reactビューエンジンとして`.jsx`を登録
await register(app);
app.get('/', (req, res) => {
const message = 'Hello World!';
res.render('index', { message });
});
app.listen(3000, () => {
console.log('> Ready on http://localhost:3000');
});
})();
export default function Index({ message }) {
return <p>{message}</p>;
}
そう、Reactをそのまま利用できるビューエンジンにしたかった
import React from 'react';
const IndexPage = (props) => {
const [message, setMessage] = React.useState('waiting...');
const onClick = () => setMessage('This is a react-ssr!');
return (
<React.Fragment>
<button onClick={onClick}>Click Me</button>
<p>Message from state: {message}</p>
</React.Fragment>
);
};
export default IndexPage;
↓↓↓
想像してみてほしい
const posts = [
{ id: 1, body: 'This is a first post.' },
{ id: 2, body: 'This is a second post.' },
{ id: 3, body: 'This is a last post.' },
];
app.get('/', (req, res) => {
res.render('index', { posts });
});
app.get('/posts/:postId', (req, res) => {
const { postId } = req.params;
const post = findById(postId);
res.render('post', { post });
});
この古き良きデータ流しを、
import React from 'react';
const IndexPage = ({ posts }) => {
return (
<React.Fragment>
{posts.map((post, index) => {
return (
<p key={index}>
<a href={'/posts/' + post.id}>{post.body}</a>
</p>
);
})}
</React.Fragment>
);
};
export default IndexPage;
↓↓↓
http://localhost:3000
import React from 'react';
const PostPage = ({ post }) => {
return (
<React.Fragment>
<p>{post.body}</p>
</React.Fragment>
);
};
export default PostPage;
↓↓↓
http://localhost:3000/posts/1
Reactで扱える喜びを...!!!
NEXT.jsとの比較
データの非同期取得
クライアント側のデータ非同期取得は、どちらもReact.useEffect()
で非同期関数を処理する必要がある。
NEXT.jsの場合
import React, { useState, useEffect } from 'react';
const HomePage = () => {
const [posts, setPosts] = useState([]);
useEffect(() => {
// componentDidMount like
fn() {
const res = await fetch('http://localhost:4000/api/posts');
const posts = await res.json();
setPosts(posts);
}
fn();
return () => {
// componentWillUnmount like
};
}, []);
return (
<React.Fragment>
{posts.map((post, index) => {
return (
<p key={index}>
<a href={'/posts/' + post.id}>{post.body}</a>
</p>
);
})}
</React.Fragment>
);
};
export default HomePage;
しかしこれではSSRされないのでSEO対策にならない。
仕方なく以下のようなNEXT.js wayに従って書き直す。
import React from 'react';
const HomePage = ({ posts }) => {
return (
<React.Fragment>
{posts.map((post, index) => {
return (
<p key={index}>
<a href={'/posts/' + post.id}>{post.body}</a>
</p>
);
})}
</React.Fragment>
);
};
HomePage.getInitialProps = async () => {
const res = await fetch('http://localhost:4000/api/posts');
const posts = await res.json();
return { posts };
};
export default HomePage;
だいぶすっきりしたものの、非同期データをSSRさせたい場合、このgetInitialProps
という魔法の呪文を唱える必要がある。
react-ssrの場合
すでに紹介したように、view template engineとして機能するため、APIサーバーから取得したデータをそのままビューへ渡せば良い。
そもそも同一サーバーならAPIサーバーを建てる必要もなく、直接DBから取得したデータをビューへ渡せばpropsから取得データへアクセスできる。
(async () => {
const posts = await fetchFromDbOrAPIServer();
app.get('/posts', (req, res) => {
res.render('posts', { posts });
});
})();
import React from 'react';
const HomePage = ({ posts }) => {
return (
<React.Fragment>
{posts.map((post, index) => {
return (
<p key={index}>
<a href={'/posts/' + post.id}>{post.body}</a>
</p>
);
})}
</React.Fragment>
);
};
export default HomePage;
ユーザー認証
NEXT.jsの場合
iaincollins/next-authというライブラリがあるが、これもクライアント側ではgetInitialProps
を利用する。
しかしサーバー側の使い方が、NEXT.js wayなサーバーの書き方をする必要があったり、そもそもドキュメントが充分とは思えないのであまり使う気にはなれなかった。
const next = require('next')
const nextAuth = require('next-auth')
const nextAuthConfig = require('./next-auth.config')
require('dotenv').load()
const nextApp = next({
dir: '.',
dev: (process.env.NODE_ENV === 'development')
})
nextApp.prepare()
.then(async () => {
const nextAuthOptions = await nextAuthConfig()
const nextAuthApp = await nextAuth(nextApp, nextAuthOptions)
console.log(`Ready on http://localhost:${process.env.PORT || 3000}`)
})
.catch(err => {
console.log('An error occurred, unable to start the server')
console.log(err)
})
↑を読んでこれから何が起こるのかわかるエスパーはいるのだろうか。
react-ssrの場合
react-ssrはExpressのview template engineとして振る舞うので、古き良きExpress wayなユーザー認証を利用できる。
例えばメールアドレス・パスワードによる認証であればpassportとpassport-localを利用する。
ソースコードは長くなるため割愛するが、その分サーバー側で柔軟な調整ができる。
- ユーザーデータのシリアライズ・デシリアライズ方法の調整
- sessionの保存方法の調整(たとえばDBにsession情報を保存する等)
- リダイレクトURLの柔軟な調整(条件分岐等)
サーバー側のバグはクリティカルであることが多い。
「なんでも屋」すぎるライブラリには頼らず、小さくて優秀なライブラリを複数利用しつつ細かい調整は自前で実装するのがベストプラクティスだと筆者は考える。
react-ssrの実装サンプル
さいごに
最終的にreact-ssrの宣伝っぽくなってしまってすみません。
ですが古き良きビューエンジンの形でReactを利用したいと思ったことはありませんか?
小規模開発でもわざわざAPIサーバーとクライアントを分けなければならない煩雑さを感じたことはありませんか?
そういった疑問を感じたことがある方にとって、少しでも可能性が広がる記事になれば幸いです。
筆者の稚拙な技術記事であるため、間違い等はコメントにてご指摘ください。
ここまで読んでくださりありがとうございました。
ちなみに私はベテルギウスが好きです。(一番高いところにあるから。)