React Router のv5.1
で Hooks API が導入された。
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
直下のコンポーネントだけではない。深い階層のコンポーネントであっても使える。上記の例では、Child
もuseParams
を使っている。
useLocation
とuseHistory
も同じ要領で、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
オブジェクトなどは、そのままでuseEffect
のdeps
などに使うことが出来る。
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
で漸進的に準備を進めておくと、よいかもしれない。