yo_waka's blog

418 I'm a teapot

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.usersconst { users } = globalState に変わるだけ。 この時点だとHTML側の埋め込んでるところはそのままでいいので、他のコンテナで触ってるところがあっても安心。

些細な変更なんだけど、こうしておくと、以降のリファクタがやりやすくなる。

レスポンスタイムが伸びてる場合

  1. コンテナでAPIをコールして、dispatchを呼んでreducerでGlobalStateを更新する
  2. APIコールをPromise.all() で並列呼び出し&まとめて、GlobalStateを更新する

それでも取ってくるデータ(API)が多くてレンダリングまでが遅い場合

  1. APIコールをServiceWorkerで行い、キャッシュしておく
  2. ログイン時にprefetchを走らせて事前キャッシュしておく

という感じでどんどん展望が立てやすくなる。