React Hooks と universal-router を雑に使ってみる

練習がてら React Hooks と universal-router を雑に使ってみる話.メモ程度で解説はあまり書かない.

実際のコードは TypeScript で書いたものを CodeSandbox に上げてる.

useLocation

履歴は history ライブラリで扱う.正直, history ちょっと大きいので,小さい alternative を探したい.

history.location の変化を取れる useLocationuseStateuseEffect から作ってみる.

function useLocation(history) {
  const [location, setLocation] = useState(history.location);
  useEffect(
    () => {
      const unlisten = history.listen((location) => setLocation(location));
      return () => unlisten();
    },
    [history],
  );

  return location;
}

useRouter

今のルーティングに合う Component を返すやつを useRouter として作る.もうちょい良い関数名にしたい.

useMemoUniversalRouter をメモ化しておく.あと,router.resolvePromise を返してくるので同期的にコンポーネントとして返せない問題を React.lazy で雑に解決してみる.

function useRouter(routes, history) {
  const location = useLocation(history);
  const router = useMemo(() => new UniversalRouter(routes), [routes]);
  const [Component, setComponent] = useState('div');

  useEffect(
    () => {
      const LazyComponent = React.lazy(() => router.resolve(location.path));
      setComponent(() => LazyComponent);
    },
    [location],
  );

  return Component;
}

HistoryContext

<Link /> のために history を Context に入れる.

const HistoryContext = React.createContext(null);

<Router />

historyroutes を渡したら,パスにあった Component を render する. ついでに HistoryContext も作る.

Component は React.lazy なので, <React.Suspense/> で Loading 画面が作れる.

function Router(props) {
  const Component = useRouter(props.routes, props.history);

  return (
    <HistoryContext.Provider value={props.history}>
      <React.Suspense fallback={<div>Loading...</div>}>
        <Component />
      </React.Suspense>
    </HistoryContext.Provider>
  );
}

<Link />

<a /> をベースにした <Link /> を作る. useContext で Context からのデータを取る. useCallback でハンドラーを作る.

function Link(props) {
  const history = useContext(HistoryContext);
  const handleClick = useCallback(
    (ev) => {
      ev.prevendDefault();
      history.push(props.href);
    },
    [history, props.href],
  );

  return <a onClick={handleClick} {...props} />;
}

マウント

const history = createBrowserHistory();
const routes = [
  {
    path: '/',
    action: () => import('./pages/FirstPage'),
  },
  {
    path: '/second',
    action: () => import('./pages/SecondPage'),
  },
];

ReactDOM.render(
  <Router history={history} routes={routes} />,
  document.getElementById('app'),
);

結果

最終的にはこんな感じ

Edit Router with React Hooks