LoginSignup
8
7

More than 5 years have passed since last update.

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

Posted at

React Router

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

Router

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の処理を見ていきます。

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を見てみます。

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を見に行きます。

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=browserHistoryroutes=chiildrenを想定します。
次に、createRouterObjectを見てみます。

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

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

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の設定を見てみます。

componentWillMount
    this._unlisten = transitionManager.listen((error, state) => {
      if (error) {
        this.handleError(error)
      } else {
        this.setState(state, this.props.onUpdate)
      }
    })
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から一致するものを探しにいく。
以上までで、わかっているのは以下のとおり。

// 便宜上、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を見てみます。

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を見に行きます。

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ということがわかりました。

8
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7