かえるの井戸端雑記

開発日誌的な記事だったり備忘録だったり。まとめ記事と言うよりは、七転八倒の様子を小説みたいに読んで眺めてもらえればと。

react-routerに触れてみようとしてreact-router-reduxに触れてみた話

背景

page urlにあわせてdomの表示切り替えたりできたら格好いいなーと思ったのでそれを実現できそうなものを探す。するとreact-routerというものを見つけ、さらにredux上で動かすならreact-router-reduxかredux-routerのどっちかがいるという話になり、うむ、どういうこっちゃ、と調べていった話。

まずは何が何だかわからないのでreact-routerをとっかかりに調べてみる

いろいろあるんだけど何をどう使えばいいのかよくわからない上にこれらのlibraryの機能がそもそもよくわかってないので調べてみる。

mk.hatenablog.com

reactのルーティング機能?というかテンプレートシステムというか?にreact-routerがあります。リクエストパスに応じて画面構成要素を組み立てるのでreact-routerを使ってアプリケーションを作ると当然URLと画面は同期することができます。URLを直打ちしてもコードでURL遷移させても、適切な画面に切り替わるように作れるのです。

reduxはその際にどうかというと、URLや画面構成といったものについて素で働きかけるものではないので、URLが変化してもreduxのステートマシンにはその情報は反映されません。そこにはもう一つ上乗せするものが必要です。いくつか同じ守備領域のライブラリがあるようですが、わたしはここのところしばらく、中でも主流感のあるreact-router-reduxを用いています。

ふむふむ。少なくともreactだったらreact-routerで動作してくれる。でもreduxのstateには反映されないのでつなげるためにreact-router-reduxを使う、と。

いやまてなんか調べてたのと違うものが出てきたぞ。

qiita.com

Reactのラウター(ルーターではない)ライブラリとしてよくreact-routerが使われる。react-routerの機能は十分だが、現在表示しているページという"状態"がアプリケーションに登場する。これもできればstoreに押し込めたい。ということで登場したライブラリ。react-routerをベースにしたライブラリなので、まずreact-routerから見ていく。

こっちにはredux-react-routerと……あれっ。githubへ飛ぶとredux-routerになっている。 まあ変わったのかな。よくある。

blog.takanabe.tokyo

redxu-routerはreact-routerのAPIをReduxから利用出来るようにするもの。

Reduxではステートを1つのStoreで管理する規則があるが、redux-routerを使うことで、RouterもStoreで管理できるようになる。

んもう。どーれー使えばいいんだー。

github.com

Please check out the differences between react-router-redux and redux-router before using this library

おっとぅ。

ここを読む限り実現の仕方が結構違うみたい。あとreact-router-reduxはReact Communityのlibrary。

github.com

というか本家のprojectにいろいろ興味深いのがあるな。このあたりは後で使ってみたい。

react-router-reduxとredux-router

閑話休題。で、このlibraryはどう違うんだろう。

react-router-redux lets React Router do all the heavy lifting and syncs the url data to a history location object in the store.

redux-routerでは?

This project, on the other hand takes the approach of storing the entire React Router data inside the redux store.

最終的な処理はreact-routerに任せているのはどちらも同じで、routingの反映の流れが違う、と。流れを比較するとこうなる。

  • redux (store.routing) ↔ react-router-redux ↔ history (history.location) ↔ react-router
  • redux (store.router) ↔ redux-router ↔ react-router (via RouterContext)

うむ。よくわからん。

必要ならcontextを使え、と。contextって何さ。

qiita.com

react-router-reduxではreact-router api経由で操作して必要ならcontextを使え、store dataを直接componentに埋め込むのはreact router propertyの更新後にstore dataが更新される可能性があるため推奨できない、と言われている。dataを引き渡すのがちと面倒になる模様。

redux-routerではreduxのstore内に全部統合されているから心配は無いよとのこと。ただしrouterのdataが全てserializeできるわけではなくなる(componentやmethodが入れ込まれるため)ため、debugしづらくなるねとかの問題がある。

気をつけながらreact-router-redux使うのがいいかなあ。

react-router-reduxを使ってみる

install library

というわけでまずはnpm install --save react-router react-router-reduxでinstall。

あ、使い方を探してたらここから良さそうな解説pageを発見。なんで今になって。

qiita.com

なるほど引用すると、

  • react-router => reactのためのrouter
  • react-router-redux => react-routerとreduxを繋いでくれる人(シンプル)
  • redux-router => react-routerとreduxを繋いでくれる人(多機能)

ということだったのね。

……まあもういいさ。先へ進もう。

しかし具体的でなくてもうちょっと抽象的な使い方が知りたい(欲張りな

本家のtutorialでわかるかな。

GitHub - reactjs/react-router-redux: Ruthlessly simple bindings to keep react-router and redux in sync

まずはimportして、と。initialStaterouting: routerReducerを登録。で、historyを追加。

renderも少し単純化して追加。

  ReactDOM.render(
    <Provider store={store}>
      <Router history={history}>
    <Route path="/" component={App}>
        </Route>
      </Router>
    </Provider>,
    document.querySelector('.content-router')
  );

んでもってindex.htmlにも

を追加。

そしてerror。せやろな。

error対処

sync.js:37 Uncaught TypeError: Cannot read property ‘getState’ of undefined

map fileからたどってみるとhistoryの定義がおかしいらしい。

あ。historyがstoreを必要としている。定義の順番が逆だった。

sync.js:104 Uncaught TypeError: Cannot read property 'listen' of undefined

でもやっぱり同じところでerrorが出てるな。ええと。現在が、

const initialState = {
  value: null,
  routing: routerReducer
};
const store = createStore(
  formReducer,
  initialState,
  (process.env.NODE_ENV !== 'production') ? window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() : undefined
);
const history = syncHistoryWithStore(browserHistory, store)

こうで。公式が。

// Add the reducer to your store on the `routing` key
const store = createStore(
  combineReducers({
    ...reducers,
    routing: routerReducer
  })
)

// Create an enhanced history that syncs navigation events with the store
const history = syncHistoryWithStore(browserHistory, store)

こうで。あれ。stateじゃなかった。reducerだった。がくっ。

combineReducersもimportして修正。

でもformReducerをどうすればいいのかわからない。

qiita.com

hash keyがついてないならlistでいけそうだな。でもroutingとかついてるんだよな。どうしよ。適当に付けるか。

適当にformとかつけたらerrorが出た。

Uncaught Error: Reducer "form" returned undefined during initialization. If the state passed to the reducer is undefined, you must explicitly return the initial state. The initial state may not be undefined.

Oh…。

試しにrouteReducerをcomment outして見たけど同じerrorが起きたのでなんか別の意味で間違っているっぽい。

[http://stackoverflow.com/questions/36619093/why-do-i-get-reducer-returned-undefined-during-initialization-despite-pr:embed:cite]

うーん。同じerrorだけど違う。まだうまくいかない。

[https://github.com/reactjs/react-router-redux/issues/348#issuecomment-286657767:embed:cite]

あ、出てきた。まだerror吐いているけどこれはinitalizeのvalueの問題だ。

test.jsx:51 Uncaught TypeError: Cannot read property 'value' of null

あー。nullにしたから。

これを

function formReducer(state=null, action) {

こう修正

function formReducer(state="", action) {

こうなった。

warning.js:10 Unexpected key "value" found in previous state received by the reducer. Expected to find one of the known reducer keys instead: "formReducer", "routing". Unexpected keys will be ignored.
test.jsx:89 Uncaught ReferenceError: App is not defined

そういえば<Route path="/" component={App}>で読んでいるcomponent=Appは未定義だった。そりゃ動かないね。

RouteTestAppに名前を変えて、HelloAppと同じ内容で追加。

お。pageにRouteTestAppで定義した内容が表示された。良きかな。

warning.js:36 Warning: Accessing PropTypes via the main React package is deprecated. Use the prop-types package from npm instead.
warning.js:10 Unexpected key "value" found in previous state received by the reducer. Expected to find one of the known reducer keys instead: "formReducer", "routing". Unexpected keys will be ignored.

これはどうしよう。

ちなみに追加した内容は以下の通り

import { Router, Route } from 'react-router'
import { createBrowserHistory } from 'history';
import { syncHistoryWithStore, routerReducer } from 'react-router-redux'
import RouteTestApp from '/components/RouteTest';

const reducers = Object.assign(
  {},
  {routing: routerReducer},
  reflectFormReducer,
);


// TODO: check duplication
const initialState = Object.assign(
  {},
  reflectFormStore,
);


const store = createStore(
  combineReducers(reducers),
  initialState,
  (process.env.NODE_ENV !== 'production') ? window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_E\
XTENSION__() : undefined
);


const history = syncHistoryWithStore(createBrowserHistory(), store)

ちゃっかりreflectFormStoreとかついているけれど、作業中に飽きてfileを分割した結果固有のReducerやAction、Containerもcomponentの中に定義することにした。だからimportが一つ増えてimport { reflectFormStore, reflectFormReducer, ReflectFormContainer } from '/components/ReflectForm';というのがあったりする。

いやこう、ここでreducerをcombineしているのを見て、あれ、分けた方がsourceの粒度がそろうな、と思ったので……。

renderにはこれを追加。

  ReactDOM.render(
    <Provider store={store}>
      <Router history={history}>
        <Route path="/" component={RouteTestApp}>
        </Route>
      </Router>
    </Provider>,
    document.querySelector('.router')
  );

index.htmlはこう修正。

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Hello React</title>
  </head>
  <body>
    <div class="content"></div>
    <div class="content2"></div>
    <div class="router"></div>
    <script src="/js/test.js"></script>
  </body>
</html>

/components/RouteTestはこう。

import React from 'react';

export default class RouteTestApp extends React.Component {
  render() {
    return (
      <div>
    react-recux-router test "/"
      </div>
    );
  }
}

warningの対処

Accessing PropTypes via the main React package is deprecated. Use the prop-types package from npm instead.

Unexpected key "value" found in previous state received by the reducer. Expected to find one of the known reducer keys instead: "routing", "reflectForm". Unexpected keys will be ignored.

めっちゃ調べてみたけど、ようするにこれらは本当にただの警告なのでは?

前者は最近のversionのreactの仕様になって変わった処理の話だし(違反してないはずだけど。書き換えたから)。

後者はreduxでのreducerの定義の話で、propsを呼ぶときにここで定義したkeyを使えよという警告。

警告というか注意書きか。

urlを表示させてみる

とりあえずcomponentを定義して読み込んでいるんだから適当なvalueを送ってみよう。storeにいれればいいのかRouteに何か定義すればいいのか。

qiita.com

ほうほう。こう呼べるのか。それなら。

これで。

  ReactDOM.render(
    <Provider store={store}>
      <Router history={history}>
        <Route path="/" component={RouteTestApp} message="root path"> </Route>
      </Router>
    </Provider>,
    document.querySelector('.router')
  );

こうだ。

export default class RouteTestApp extends React.Component {
  render() {
    return (
      <div>
        react-recux-router test "{this.props.message}"
      </div>
    );
  }
}

そしてこうだ。

browser.js:49 Warning: You should not use <Route component> and <Route children> in the same route; <Route children> will be ignored

このやろう。renderのtimingでthis.propsをlogで見てみよう。

Object {match: Object, location: Object, history: Object, staticContext: undefined}

よくわからぬ。いや展開してみたらわかった。ここのlocation.pathnameがurlだ。

以下のような感じにしてみる。

export default class RouteTestApp extends React.Component {
  render() {
    console.log(this.props);
    return (
      <div>
        react-recux-router test "{this.props.location.path}"
      </div>
    );
  }
}

これで表示できるかな。

できた。けど怒られた。

browser.js:49 Warning: You should not use <Route component> and <Route children> in the same route; <Route children> will be ignored

前の話にあったreact-router-reduxではreact-router api経由で操作して必要ならcontextを使えという話だろうなあ。

……正直redux上でreact-router使う必要があるのかどうか悩ましくなってきたぞ。ちょっと考えよう。

reduxを試してみた(4日目) - redux-react-routerを試す - Qiita

react-routerの特徴 ドキュメントの構造とURLの構造をマッピングするライブラリ。最初ドキュメントを読んだ時理解できなかったけどこんなことらしい。端的に言えば、画面上のコンポーネントをURLによって切り替えたい部分毎にを用意し、対応するcomponentを下記のように定義しておくと、URLがマッチする時にコンポーネントを切り替えてくれる。

React × Redux 初心者入門(3日目:react-router-reduxでルーティングを実装する) - Hirooooo’s Labo

react-routerを使用し、さらにredux環境下でstate管理を行うためのパッケージとしてreact-router-reduxを採用しました。

うむ。このstate管理がどこまでの話なのかがわからない。

react-routerとredux-simple-routerとredux-react-routerとreact-router-reduxとredux-router - Qiita

react-router-redux reduxはstateで状態管理してreactに教えてくれてるわけだからurlの変更もstateとして管理したいぜ。 って言うのを叶えてくれる人。

それだけだったら別にいいかな。

今現在僕が作っているpage structureでは、urlというかpageごとにbundleされたjsが配置されているという構造だから、stateが引き継がれることはない。

単に描画時にurlを取得してきてそれに併せて表示するmenuを切り替えたいくらいしかやりたいことは考えてないからurlの管理まですると過剰機能。

たぶんこれSPAなどを組むときには必要な機能なんだろうなあ。

じゃあreact-routerだけでいれこんでみる。

Route.js:53 Uncaught TypeError: Cannot read property 'route' of undefined

えー。

Usage with React Router · Redux

historyとかも消してたけどここ見ると必要っぽいなぁ。

入れてみたらerrorは消えた。

今度はthis.props.match.pathを呼び出してもwarningはなし。でもhistoryを扱うのは変わってないけどいいのかなこれ。

ってあれ?

うっかりreact-router-reduxまで復活させたけど前と同じwarningが出てこない。

……なんなんだこれ。まあ動くからいいけど。

stackoverflow.com

ああやはりcontext使えという話か。

qiita.com

あー。context使うならcomponent作らないといけないのか。面倒な。

import React from 'react';
import PropTypes from 'prop-types';


export default class RouteTestApp extends React.Component {
  getChildContext() {
    return {
      location: this.props.location
    }
  }
  render() {
    return (
      <Child />
    );
  }
}


RouteTestApp.childContextTypes = {
  location: PropTypes.object,
};


class Child extends React.Component {
  render() {
    return (
      <div>
        react-router-redux test: current path = "{this.context.location.pathname}"
      </div>
    );
  }
}

Child.contextTypes = {
  location: PropTypes.object,
};

できた。表示できることも確認。

ちょっと振り返ってみる

react-router-reduxとredux-routerの違いは何かというと、

  • redux (store.routing) ↔ react-router-redux ↔ history (history.location) ↔ react-router
  • redux (store.router) ↔ redux-router ↔ react-router (via RouterContext)

こうだと先に上がっていた。

今にして思えばぼんやりと理解できる話で、react-router-reduxではstore.routingを使って情報を保持するし、reduxの中ではstateとしてhistory.locationというのを持つことでrouting関係のstateを保持していた世という話。

redux-routerはそのあたりredux-routerで全部やるよと言っているらしい。store.routerにdataはいれておくから気になったら見てね、と。

source全体は?

まだあちこちいじるので後で。