React Routerは何をしているのか？ ~Router編~

## React Router
* [公式サイト](https://github.com/reactjs/react-router/blob/master/modules/Router.js)
* pathとComponentを紐付ける機能

自分でReactのアプリケーションを書いていると、タブやメニューなどでコンテンツだけを入れ替えたい時が山程あります。
onClickなどでeventを作成し、renderingするComponentを切り替えるのも手ですが、コードが煩雑になってしまいます。
React Routerはそれを上手いことまとめてくれています。
ただ使うだけでは応用が効かない、かつ自分だけ欲しいような機能を作る時に参考にもしたいため、内部を見ていこうと思います。

## Router

```js:Router.js
const Router = React.createClass({
  ...,

  getDefaultProps() {
    return {
      render(props) {
        return <RouterContext {...props} />
      }
    }
  },

  getInitialState() {
    return {
      location: null,
      routes: null,
      params: null,
      components: null
    }
  },

  ...,

  componentWillMount() {
    const { parseQueryString, stringifyQuery } = this.props
    warning(
      !(parseQueryString || stringifyQuery),
      '`parseQueryString` and `stringifyQuery` are deprecated. Please create a custom history. http://tiny.cc/router-customquerystring'
    )

    const { history, transitionManager, router } = this.createRouterObjects()

    this._unlisten = transitionManager.listen((error, state) => {
      if (error) {
        this.handleError(error)
      } else {
        this.setState(state, this.props.onUpdate)
      }
    })

    this.history = history
    this.router = router
  },

  ...,

  render() {
    const { location, routes, params, components } = this.state
    const { createElement, render, ...props } = this.props

    if (location == null)
      return null // Async match

    // Only forward non-Router-specific props to routing context, as those are
    // the only ones that might be custom routing context props.
    Object.keys(Router.propTypes).forEach(propType => delete props[propType])

    return render({
      ...props,
      history: this.history,
      router: this.router,
      location,
      routes,
      params,
      components,
      createElement
    })
  }
})
```

この3つがmount時に呼ばれるmothodとなります。
順番はgetInitialState -> componentWillMount -> renderです。
getInitilaStateはstateの初期化なので無視して良いとして、componentWillMountの処理を見ていきます。

```js:componentWillMount
  componentWillMount() {
    const { parseQueryString, stringifyQuery } = this.props
    warning(
      !(parseQueryString || stringifyQuery),
      '`parseQueryString` and `stringifyQuery` are deprecated. Please create a custom history. http://tiny.cc/router-customquerystring'
    )

    const { history, transitionManager, router } = this.createRouterObjects()

    this._unlisten = transitionManager.listen((error, state) => {
      if (error) {
        this.handleError(error)
      } else {
        this.setState(state, this.props.onUpdate)
      }
    })

    this.history = history
    this.router = router
  },
```

まず、propsに設定されていたら意味ないというwarningを出しています。
その次のcreateRouterObjectsを見てみます。

```js:createRouterObjects
  createRouterObjects() {
    const { matchContext } = this.props
    if (matchContext) {
      return matchContext
    }

    let { history } = this.props
    const { routes, children } = this.props

    if (isDeprecatedHistory(history)) {
      history = this.wrapDeprecatedHistory(history)
    }

    const transitionManager = createTransitionManager(
      history, createRoutes(routes || children)
    )
    const router = createRouterObject(history, transitionManager)
    const routingHistory = createRoutingHistory(history, transitionManager)

    return { history: routingHistory, transitionManager, router }
  },
```

propsにmatchContextが設定されていればそれを返して終了。
次に、propsからhistoryとroutesを取得します。
childrenは子要素のReactElementになります。(React Routerでは大体が`<Route ...props />`となります。)
historyのdeprecatedを確認後、RouteとComponentのtrasitionを作成します。
作成するcreateTransitionManagerを見に行きます。

```js:createTransitionManager
export default function createTransitionManager(history, routes) {
  let state = {}

  functoin isActive

  function createLocationFromRedirectInf

  let partialNextState

  function match

  function finishMatch

  let RouteGuid = 1

  function getRouteID

  const RouteHooks = Object.create(null)

  function getRouteHooksForRoutes

  function transitionHook

  function beforeUnloadHook

  let unlistenBefore, unlistenBeforeUnload

  function removeListenBeforeHooksForRoute

  function listenBeforeLeavingRoute

  function listen

  return {
    isActive,
    match,
    listenBeforeLeavingRoute,
    listen
  }
}
```

closureをまとめたobjectを返しているようです。
ここでは、exampleにしたがって、`history=browserHistory`、`routes=chiildren`を想定します。
次に、createRouterObjectを見てみます。

```js:RouterUtil.js
export function createRouterObject(history, transitionManager) {
  return {
    ...history,
    setRouteLeaveHook: transitionManager.listenBeforeLeavingRoute,
    isActive: transitionManager.isActive
  }
}
```

transitionMangerから、listenBeforeLeavingRouteとisActiveを引っこ抜いたobjectのようです。
次に、createRoutingHistoryを見に行きます。

```js:RouterUtil.js
// deprecated
export function createRoutingHistory(history, transitionManager) {
  history = {
    ...history,
    ...transitionManager
  }

  if (__DEV__) {
    history = deprecateObjectProperties(
      history,
      '`props.history` and `context.history` are deprecated. Please use `context.router`. http://tiny.cc/router-contextchanges'
    )
  }

  return history
}
```

historyとtransitionManagerのclosureをまとめたobjectを返しているようですが、どうやらdeprecatedになっているみたいですね。
ここまでで、`history`, `transitionManger`, `router`が出揃いました。
つぎにlistenerの設定を見てみます。

```js:componentWillMount
    this._unlisten = transitionManager.listen((error, state) => {
      if (error) {
        this.handleError(error)
      } else {
        this.setState(state, this.props.onUpdate)
      }
    })
```

```js:transitionManager.listen
function listen(listener) {
    // TODO: Only use a single history listener. Otherwise we'll
    // end up with multiple concurrent calls to match.
    return history.listen(function (location) {
      if (state.location === location) {
        listener(null, state)
      } else {
        match(location, function (error, redirectLocation, nextState) {
          if (error) {
            listener(error)
          } else if (redirectLocation) {
            history.transitionTo(redirectLocation)
          } else if (nextState) {
            listener(null, nextState)
          } else {
            warning(
              false,
              'Location "%s" did not match any routes',
              location.pathname + location.search + location.hash
            )
          }
        })
      }
    })
  }
```

locationを引数に取って、transitionManagerのstate.locationと比較して一致すればlistenerを実行し、それ以外は登録されているroutesから一致するものを探しにいく。
以上までで、わかっているのは以下のとおり。

```js:
// 便宜上、class名で表記する
Router.hitstory = { 
  ..browserHisotry,
  transactionManager.isActive,
  transactionManager.match,
  transactionManager.listenBeforeLeavingRoute,
  transactionManager.listen,
}

Router._unlisten = browserHistory.listen(listener)

Router.router = {
 ...history,
  setRouteLeaveHook: transitionManager.listenBeforeLeavingRoute,
  isActive: transitionManager.isActive
}
```

最後にrenderを見てみます。

```js:render
render() {
    const { location, routes, params, components } = this.state
    const { createElement, render, ...props } = this.props

    if (location == null)
      return null // Async match

    // Only forward non-Router-specific props to routing context, as those are
    // the only ones that might be custom routing context props.
    Object.keys(Router.propTypes).forEach(propType => delete props[propType])

    return render({
      ...props,
      history: this.history,
      router: this.router,
      location,
      routes,
      params,
      components,
      createElement
    })
  }
```

各stateは初期値はnull。
this.propsは`<Router ...props />`の時点で`React.createElement`処理内でgetDefaultProps()の値がつめられています。
そこで、getDefaultPropsを見に行きます。

```js:getDefaultProps
  getDefaultProps() {
    return {
      render(props) {
        return <RouterContext {...props} />
      }
    }
  },
```

renderをmethodに持つObjectを返します。
ここで`Router.props['render'] = render(props) { ... }`となります。
これをrenderに詰め替えなおしています。
この次に、定義されているすべてのpropsを削除。
最後に、詰め替え直したrenderにObjectを引数として渡しています。
値のあるpropertyは、history, routerのみとなります。

Router自体は必要なmethodを用意して、RouterContextにpropsを渡すComponentということがわかりました。