30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

React Router v5.1 で導入された Hooks API について

React Router のv5.1で Hooks API が導入された。

reacttraining.com

URL パラメータやhistoryオブジェクトなどを、より簡単に取得できるようになった。
これまでの書き方も使えるが、今後のバージョンアップで非推奨になる可能性が高いとのことなので、今のうちに Hooks を使った書き方に慣れておいたほうがいいかもしれない。
個人的にはwithRouterを使わなくてもよくなるのが便利だと感じた。

この記事の内容は以下のバージョンで動作確認している。

  • react@16.10.2
  • react-router-dom@5.1.2

今回導入された API は以下の4つ。

  • useParams
  • useLocation
  • useHistory
  • useRouteMatch

コードを見ると、どれもReact.useContextを使って実装している模様。
react-router/hooks.js at v5.1.2 · ReactTraining/react-router

問題意識

上述のように、URL パラメータなどのルーティングに関する情報を取得する方法が、変わった。
「これまでと何が変わったのか」という視点で見たほうが分かりやすいので、まず従来の記法とその問題点を紹介することで、Hooks API が何を解決しているのかを説明する。

以下は、従来の記法で URL パラメータを取得している。
/component/xxxもしくは/render/xxxにアクセスすると、xxxの部分をpageIdとして認識して画面に表示する。

import React from 'react';
import {BrowserRouter, Switch, Route} from 'react-router-dom';

const Child = ({pageId}) => {
  return <p>This is child element in {pageId}</p>;
};

const ShowPageId = ({match}) => {
  return (
    <div>
      <p>Page id: {match.params.pageId}</p>
      <Child pageId={match.params.pageId} />
    </div>
  );
};

const App = () => (
  <BrowserRouter>
    <Switch>
      <Route path="/component/:pageId" component={ShowPageId} />
      <Route
        path="/render/:pageId"
        render={({match}) => <ShowPageId match={match} />}
      />
    </Switch>
  </BrowserRouter>
);
export default App;

従来は、ルーティングに関する情報をコンポーネントに渡すには、component属性かrender属性を使う必要があった。
だが、どちらも一長一短ある。
component属性を使うと、任意のpropsを渡すことが出来ない。render属性ならそれが可能だが、そうすると今度は、ルーティングに関する情報(上記の例ではmatch)も明示的に渡さなければならなくなる。
また、子要素(上記の例ではChild)にルーティングに関する情報を渡したいときは、その場合も明示的にpropsを渡さないといけない。

Hooks API はこれらの問題を解決する。

Hooks API ならルーティングに関する情報をどこからでも得られる

コンポーネントのなかでuseParamsを使えば、component属性やrender属性を使うことなく、URL パラメータを取得できる。

import React from 'react';
import {BrowserRouter, Switch, Route, useParams} from 'react-router-dom';

const Child = () => {
  const {pageId} = useParams();
  return <p>This is child element in {pageId}</p>;
};

const ShowPageId = ({originalProp}) => {
  const {pageId} = useParams();
  return (
    <div>
      <p>Page id: {pageId}</p>
      <p>{originalProp}</p>
      <Child />
    </div>
  );
};

const App = () => (
  <BrowserRouter>
    <Switch>
      <Route path="/component/:pageId">
        <ShowPageId originalProp="You can pass any parameter." />
      </Route>
    </Switch>
  </BrowserRouter>
);
export default App;

まず、Routeコンポーネントの書き方が変わる。component属性やrender属性で描画したいコンポーネントを指定するのではなく、Routeの子コンポーネントとして記述する。
そして当該コンポーネントのなかでuseParamsを使うことで、URL パラメータを取得できる。
ルーティングに関する情報を明示的にporpsとして渡す必要はないので、通常のコンポーネントとして扱える。任意のpropsを渡せるし、渡さなくてもよい。
useParamsを使えるのは、Route直下のコンポーネントだけではない。深い階層のコンポーネントであっても使える。上記の例では、ChilduseParamsを使っている。

useLocationuseHistoryも同じ要領で、locationオブジェクトとhistoryオブジェクトを取得するための API である。

import React from 'react';
import {
  BrowserRouter,
  Switch,
  Route,
  useLocation,
  useHistory,
} from 'react-router-dom';

const HooksSample = () => {
  const location = useLocation();
  const history = useHistory();
  console.log(Object.keys(location)); // ["pathname", "search", "hash", "state"]
  console.log(Object.keys(history)); // ["length", "action", "location", "createHref", "push", "replace", "go", "goBack", "goForward", "block", "listen"]
  return <div>Sample</div>;
};

const App = () => (
  <BrowserRouter>
    <Switch>
      <Route path="/component/:pageId">
        <HooksSample />
      </Route>
    </Switch>
  </BrowserRouter>
);
export default App;

なお、useHistoryは今後実装が予定されているuseNavigateの代用品のような位置付けらしい。今からuseHistoryを使っていくことで、同じ Hooks API であるuseNavigateに移行しやすくなる、ということだろう。

withRouter は非推奨になる可能性が高い

実は今までも、ルーティングに関する情報を明示的にコンポーネントに渡さなくても、withRouterを使えばそれを取得することが出来た。

import React from 'react';
import {BrowserRouter, Switch, Route, withRouter} from 'react-router-dom';

const ShowPageId = ({match}) => {
  return (
    <div>
      <p>Page id: {match.params.pageId}</p>
    </div>
  );
};
const WrappedShowPageId = withRouter(ShowPageId);

const App = () => (
  <BrowserRouter>
    <Switch>
      <Route path="/render/:pageId" render={() => <WrappedShowPageId />} />
    </Switch>
  </BrowserRouter>
);
export default App;

/render/xxxにアクセスすれば、URL パラメータを取得できていることを確認できる。

だが Hooks API が導入された今では、withRouterを使う積極的な理由はないはず。
しかも公式ブログによれば、今後のリリースで非推奨になる可能性が高いとのこと。

although withRouter is not deprecated in 5.1, it's a weird API when you can compose your state with hooks instead. It will also most likely be deprecated in a future release.

match オブジェクト等は同一性が保たれる

これは今回のバージョンアップで入ったものではなく以前からそうだったが、React Router から渡されるmatchオブジェクトなどは、コンポーネントがレンダリングされ直しても、同じ参照のオブジェクトが渡される。値が同じだけの別のオブジェクト、にはならない。

import React, {useState, useEffect} from 'react';
import {BrowserRouter, Switch, Route} from 'react-router-dom';

const paramsList = [];
const locationList = [];
const historyList = [];

const CountUp = props => {
  const [state, setState] = useState(0);

  const {match, location, history} = props;
  const {params} = match;

  useEffect(() => {
    paramsList.push(params);
    locationList.push(location);
    historyList.push(history);

    if (paramsList.length >= 2) {
      const prevIndex = paramsList.length - 2;
      const currentIndex = paramsList.length - 1;

      // いずれも常に true になる
      console.log(Object.is(paramsList[prevIndex], paramsList[currentIndex]));
      console.log(
        Object.is(locationList[prevIndex], locationList[currentIndex])
      );
      console.log(Object.is(historyList[prevIndex], historyList[currentIndex]));
    }
  });

  return (
    <div>
      <p>{state}</p>
      <button
        type="button"
        onClick={() => {
          setState(s => s + 1);
        }}
      >
        click
      </button>
    </div>
  );
};

const App = () => (
  <BrowserRouter>
    <Switch>
      <Route path="/component/:pageId" component={CountUp} />
    </Switch>
  </BrowserRouter>
);
export default App;

以下のように書き換えて Hooks API を使うようにしても、同じ結果になる。

@@ -1,15 +1,23 @@
 import React, {useState, useEffect} from 'react';
-import {BrowserRouter, Switch, Route} from 'react-router-dom';
+import {
+  BrowserRouter,
+  Switch,
+  Route,
+  useParams,
+  useLocation,
+  useHistory,
+} from 'react-router-dom';

 const paramsList = [];
 const locationList = [];
 const historyList = [];

-const CountUp = props => {
+const CountUp = () => {
   const [state, setState] = useState(0);

-  const {match, location, history} = props;
-  const {params} = match;
+  const params = useParams();
+  const location = useLocation();
+  const history = useHistory();

   useEffect(() => {
     paramsList.push(params);
@@ -47,7 +55,9 @@ const CountUp = props => {
 const App = () => (
   <BrowserRouter>
     <Switch>
-      <Route path="/component/:pageId" component={CountUp} />
+      <Route path="/component/:pageId">
+        <CountUp />
+      </Route>
     </Switch>
   </BrowserRouter>
 );

そのためmatchオブジェクトなどは、そのままでuseEffectdepsなどに使うことが出来る。

useRouteMatch

useRouteMatchは他の Hooks API とは使い方が異なり、Routeコンポーネントの代わりに使う。

以下のコードでは、Routeコンポーネントを使ってAdminMenuの表示を制御している。
例えば/user/1にアクセスしたときは共通メニューしか表示されないが、admin/1にアクセスすると管理者限定メニューも表示される。

import React from 'react';
import {BrowserRouter, Route} from 'react-router-dom';

const AdminMenu = ({match}) => {
  return (
    <>
      <h2>管理者限定メニュー</h2>
      <p>管理者ID: {match.params.adminId}</p>
      <ul>
        <li>ユーザー追加</li>
        <li>ユーザー削除</li>
      </ul>
    </>
  );
};

const Menu = () => {
  return (
    <div>
      <h1>Menu</h1>
      <h2>共通メニュー</h2>
      <ul>
        <li>データ閲覧</li>
        <li>データ編集</li>
      </ul>
      <Route path="/admin/:adminId" component={AdminMenu} />
    </div>
  );
};

const App = () => (
  <BrowserRouter>
    <Menu />
  </BrowserRouter>
);
export default App;

同じことをuseRouteMatchを使って実装したのが以下。

@@ -1,5 +1,5 @@
 import React from 'react';
-import {BrowserRouter, Route} from 'react-router-dom';
+import {BrowserRouter, useRouteMatch} from 'react-router-dom';

 const AdminMenu = ({match}) => {
   return (
@@ -15,6 +15,9 @@ const AdminMenu = ({match}) => {
 };

 const Menu = () => {
+  const match = useRouteMatch({
+    path: '/admin/:adminId',
+  });
   return (
     <div>
       <h1>Menu</h1>
@@ -23,7 +26,7 @@ const Menu = () => {
         <li>データ閲覧</li>
         <li>データ編集</li>
       </ul>
-      <Route path="/admin/:adminId" component={AdminMenu} />
+      {match && <AdminMenu match={match} />}
     </div>
   );
 };

v6 への準備

v5.1では、これまでの書き方も問題なく使える。だが今後のリリースで非推奨になる可能性が高い。
Hooks API を積極的に使うことは、今後のメジャーバージョンアップへの準備になる。

Although they are not deprecated in 5.1, the and APIs have several quirks that just aren't needed (see the discussion in useParams above) in a world with hooks. We will most likely deprecate these APIs in a future release.

両方の書き方を使えるv5.1で漸進的に準備を進めておくと、よいかもしれない。

参考資料

『基礎からわかるTCP/IP ネットワークコンピューティング入門 第3版』を読んだ

ネットワークの勉強のために読んだ。

www.ohmsha.co.jp

勉強したい分野の一つにネットワークがあり、何かよい本はないかと探していたとき、以下のツイートが流れてきた。

twitter.com

鹿野さんがここまで言うなら間違いないだろうと思って購入した。
このツイートがなければ本書を知ることはなかっただろうし、ネットワークへの興味が薄い時期だったら見逃していただろう。やっぱり宣伝は大切だ。あとは信用。信用があるからこそ、宣伝が意味を持つ。

コンピュータ同士がどのようにつながって通信を行っているのかについて、基本的なところから説明されている。まさに自分が知りたいことだったので、よかった。

断片的な知識がつながってきて、モヤモヤしていたものが少しずつクリアになってきた感覚がある。
ネットワークの説明を読んでも分かったような分からないような気分になることが多いのだが、本書は分かった気にさせてくれる部分が多い。ブラックボックスにせず、具体的な仕組みを解説してくれるからだろうか。
単なる用語の説明ではなく、何のためにそのような処理を行っているのか、その処理の意味や役割についても説明しているのがよい。入門書ではそれが大切だと思う。
例えば、送信先の機器を一意に特定するための値として、なんでIPアドレスMACアドレスの2つがあるのかよく分かっていなかったが、それぞれの役割や使われ方について説明されており、自分なりに納得できた。

ネットワークの入門書ではあるがコンピュータについても章が割かれており(第3章)、これも出来がよかった。コンパクトな「コンピュータシステム入門」のような内容で、自分のような初心者にとってはこれだけでも読む価値がある。

インターネットを実現するための技術としてTCP/IPは今後も使われ続けるだろうし、HTTP3QUICのような新しい技術も既存の技術の延長線上にあるわけだから、ネットワークについて学ぶのは投資効率がよいと思う。
ただ、なかなか手を動かしづらい分野なので、それが難しい。経験上、何かを作ってみるのが最も学習効率が高いのだが、ネットワークの場合は何を作ればいいのだろうか。
今は他に作りたいものがあるのだが、ネットワークの知識が問われるようなものをいずれ作ってみたい気持ちもある。