読者です 読者をやめる 読者になる 読者になる

あいつの日誌β

あいつの日誌です。

react-router@v4 を調査した

あらすじ

react-router@v4 にしたら色々な変更が入っていて全く動かなくなったので react-router の機能を確認しました。

追記: 2017-03-23 16:30

withRouter について追記しました。

言及すること

  • <Route> の挙動が v3 と異なる
  • <Switch><Route exact ...> の挙動を理解して対処しましょう
  • browserHistory.push から `this.props.history.push へ変わった
  • <NavLink> が追加されました
  • Nested Routes が廃止された?
  • withRouter

準備

create-react-app practice-react-router4 && cd $_
npm install --save react-router-dom

<Route>

最初に src/App.js を以下のように修正します。

import React from 'react'
import {
  BrowserRouter as Router, Switch, Route, Redirect,  Link, NavLink, withRouter
} from 'react-router-dom'

const Home = () => (<h2>Home</h2>)
const About = () => (<h2>About</h2>)
const User = () => (<h2>User</h2>)

const App = () => (
  <Router>
    <div style={{padding: "60px"}}>
      <Route path="/" component={Home} />
      <Route path="/about" component={About}/>
      <Route path="/:user" component={User}/>
      <Route component={NoMatch}/>
    </div>
  </Router>
)

export default App 

この状態で http://localhost:3000/, http://localhost:3000/okamuuu, `http://localhost:3000/about にアクセスするとそれぞれ以下の画面が表示されます。

open http://localhost:3000/

<div data-reactroot="" style="padding: 60px;">
  <h2>Home</h2>
  <h2>NoMatch</h2>
</div>

open http://localhost:3000/okamuuu

<div data-reactroot="" style="padding: 60px;">
  <h2>Home</h2>
  <h2>User: okamuuu</h2>
  <h2>NoMatch</h2>
</div>

open http://localhost:3000/about

<div data-reactroot="" style="padding: 60px;">
  <h2>Home</h2>
  <h2>About</h2>
  <h2>User: okamuuu</h2>
  <h2>NoMatch</h2>
</div>

このような <Route> の使い方をすると、version4 では URL にマッチする component を全て含んでしまいます。

<Swtich>

先ほどの <Route>s の直上に <Switch> を加えます。

 const App = () => (
   <Router>
     <div style={{padding: "60px"}}>
+      <Switch>
         <Route path="/" component={Home} />
         <Route path="/about" component={About}/>
         <Route path="/:user" component={User}/>
         <Route component={NoMatch}/>
+      </Switch>
     </div>
   </Router>
 )

この状態で http://localhost:3000/, http://localhost:3000/okamuuu, `http://localhost:3000/about にアクセスすると全て以下の画面が表示されます。

<div data-reactroot="" style="padding: 60px;">
  <h2>Home</h2>
</div>

これは最初に Match する要素が発見され次第、その要素だけを返すからです。いまのままでは全ての location に対して <Home> を返してしまうので以下のように正確に Match した場合だけ判定するように <Route>exact=true を追加します。

open http://localhost:3000/

<div data-reactroot="" style="padding: 60px;">
  <h2>Home</h2>
</div>

open http://localhost:3000/okamuuu

<div data-reactroot="" style="padding: 60px;">
  <h2>User: okamuuu</h2>
</div>

open http://localhost:3000/about

<div data-reactroot="" style="padding: 60px;">
  <h2>About</h2>
</div>

browserHistory.push

以下のように props で受け取る形式に変わっています。

-const User = (props) => (<h2>User: {props.match.params.user}</h2>)
+const User = (props) => (
+  <div>
+    <h2>User: {props.match.params.user}</h2>
+    <button onClick={() => props.history.push("/")}>Back to Home</button>
+  </div>
+)

<NavLink>

src/App.js を以下のように修正します。

 const App = () => (
   <Router>
     <div style={{padding: "60px"}}>
+      <ul>
+        <li><NavLink to="/">Home</NavLink></li>
+        <li><NavLink to="/about">About</NavLink></li>
+        <li><NavLink to="/okamuuu">User</NavLink></li>
+      </ul>
       <Switch>
         <Route exact path="/" component={Home} />

Match する場合は className に active が追加されます。

open http://localhost:3000/

<ul>
    <li><a class="active " href="/">Home</a></li>
    <li><a href="/about">About</a></li>
    <li><a href="/okamuuu">User</a></li>
</ul>

open http://localhost:3000/okamuuu

<ul>
    <li><a class="active " href="/">Home</a></li>
    <li><a href="/about">About</a></li>
    <li><a class="active " href="/okamuuu">User</a></li>
</ul>

open http://localhost:3000/about

<ul>
    <li><a class="active " href="/">Home</a></li>
    <li><a class="active  href="/about">About</a></li>
    <li><a href="/okamuuu">User</a></li>
</ul>

便利な機能ですがこのままだと / も Active になってしまいます。<Route> の時のように extract を指定します。

-        <li><NavLink to="/">Home</NavLink></li>
+        <li><NavLink exact to="/">Home</NavLink></li>

また CSS in JS したい場合は className を各自で好きなように指定したい場合は以下のようにします。

CSS in JS したい

<NavLink
  to="/about"
  activeStyle={{
    fontWeight: 'bold',
    color: 'red'
   }}
>About</NavLink>

className を好きにしたい

<NavLink
  to="/about"
  activeClassName="selected"
>About</NavLink>

個人的な意見ですが NavLink を使うような components はロジックと分離したいのであまり使わないかもしれないのですが一応使い方は覚えておいたほうがいいと思います。

ロジックの分離: About がクリックされたら親コンポーネントに「今 About が押されたよ。あとよろ」と実装して web components として再利用しやすい状態にしたい。

Nested Routes 廃止?

v3 では共通の components を使用したい場合は {this.props.children} と書いて Nested Routes していたのですが、v4 からはそうではなくなっているようです。

withRouter

Home へ戻るボタンを追加したいのですが、ボタン自体に hisotry オブジェクトを渡さない場合、呼び出し元で hisotry.push 関数を実行したいのですが react-router@v4 から browserHistory が import できなくなっています。

代わりに withRouter を使います。

edit src/App.js

const HomeButton = ({onClick}) => (
  <button onClick={onClick}>push Home</button>
)

const Routes = withRouter((props) => {
  const {push} = props.history

  return (
    <div style={{padding: "60px"}}>
      <ul>
        <li><NavLink exact to="/">Home</NavLink></li>
        <li><NavLink to="/about">About</NavLink></li>
        <li><NavLink to="/okamuuu">User</NavLink></li>
      </ul>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route exact path="/about" component={About}/>
        <Route path="/:user" component={User}/>
        <Route component={NoMatch}/>
      </Switch>
      <HomeButton onClick={() => push("/")} />
    </div>
  )
})

const App = () => (
  <Router>
    <Routes />
  </Router>
)

まとめ

というわけで v3 から v4 に移行する場合、小さなアプリはいいと思いますがそうでない場合はちょっと面倒くさいかもしれません。こちらからは以上です。