HTMLに埋め込まれたグローバルなJSONをReact Context APIとHooksで地道にリファクタしていく
新年の書き初めにReact Hooksでいろいろ習作を書いたりしてた。
WebアプリケーションだとサーバサイドからHTMLビューにJSONを書き出して、フロントエンドでそれを使うというのはよくやるやつだと思う。
こんなやつ(例はRailsのERB)
<script> window.GLOBAL = {}; window.GLOBAL.users = <% json_escape @users %> </script>
特にマスタデータ系は昔からこんな感じでやってるのが結構多いんじゃないかなと思う。
フロントエンドからはいつでも const users = GLOBAL.users
で引いてこれるので便利っちゃ便利。
そこそこデカいJSONでもあまり気にせず書き出せるのも気楽でよいっちゃよい。
ただ、ビュー内で同じようなのが増えてくると、ビューが書き出されるまでのレスポンスタイムが長くなるのと、いろんな箇所で GLOBAL
を参照してしまいがちでリファクタリング時のネックにもなりがち。HTML内なので型定義もやりづらく、バグの温床にもなる。
これはReact Context APIとHooksで書き直すことができる。
勝手にグローバルステートパターンと呼んでる。
Fluxでアプリケーションを作っているならこんな感じのストアを用意する。
import React, { useReducer } from 'react'; export type State = {| users: Array<User> |}; // 何もしない const reducer = (state = {}, _action): State => state; const Store = React.createContext(); const Provider = ({ children }) => { const [state, dispatch] = useReducer(reducer, window.GLOBAL); return ( <Store.Provider value={{ state, dispatch }}> {children} </Store.Provider> ); }; export { Store, Provider };
useReducer に window.GLOBAL
を指定してあげるのがポイント。
あとはコンテナでProviderを使って、コンポーネントでuseContextすればいい。
// @flow import React from 'react'; import { Store as GlobalStore, Provider as GlobalStateProvider } from './flux/stores/global_store'; const ListItem: React.FC = (props: { user: User }) => { const { user } = props; return ( <ul> <li>name: {user.name}</li> <li>email: {user.email}</li> </ul> ); }; const List: React.FC = () => { const { globalState } = useContext(GlobalStore); const { users } = globalState; return ( <React.Fragment> <h2>List</h2> {users.map(user => <ListItem user={user} />)} </React.Fragment> ); }; const Container: React.FC = () => ( <GlobalStateProvider> <List /> </GlobalStateProvider> ); window.onload = () => { const el = document.getElementById('root'); if (el) { ReactDOM.render(<Container />, el); } };
使う側からは、const users = GLOBAL.users
が const { users } = globalState
に変わるだけ。
この時点だとHTML側の埋め込んでるところはそのままでいいので、他のコンテナで触ってるところがあっても安心。
些細な変更なんだけど、こうしておくと、以降のリファクタがやりやすくなる。
レスポンスタイムが伸びてる場合
- コンテナでAPIをコールして、dispatchを呼んでreducerでGlobalStateを更新する
- APIコールをPromise.all() で並列呼び出し&まとめて、GlobalStateを更新する
それでも取ってくるデータ(API)が多くてレンダリングまでが遅い場合
- APIコールをServiceWorkerで行い、キャッシュしておく
- ログイン時にprefetchを走らせて事前キャッシュしておく
という感じでどんどん展望が立てやすくなる。