React Router
- 公式サイト
- pathとComponentを紐付ける機能
自分でReactのアプリケーションを書いていると、タブやメニューなどでコンテンツだけを入れ替えたい時が山程あります。
onClickなどでeventを作成し、renderingするComponentを切り替えるのも手ですが、コードが煩雑になってしまいます。
React Routerはそれを上手いことまとめてくれています。
ただ使うだけでは応用が効かない、かつ自分だけ欲しいような機能を作る時に参考にもしたいため、内部を見ていこうと思います。
Router
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() {
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() {
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を見に行きます。
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を見てみます。
export function createRouterObject(history, transitionManager) {
return {
...history,
setRouteLeaveHook: transitionManager.listenBeforeLeavingRoute,
isActive: transitionManager.isActive
}
}
transitionMangerから、listenBeforeLeavingRouteとisActiveを引っこ抜いたobjectのようです。
次に、createRoutingHistoryを見に行きます。
// 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の設定を見てみます。
this._unlisten = transitionManager.listen((error, state) => {
if (error) {
this.handleError(error)
} else {
this.setState(state, this.props.onUpdate)
}
})
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() {
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() {
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ということがわかりました。