react-router を読んでみたので、そのメモ。
対象読者
一回は、react-router を使ったことがある人。
react-router とは?
React のページ遷移を管理するライブラリ。
使い方は、下のように、BrowserRouter と Switch で囲った部分の 各 Route が、ページになる (v5)。
最新の v6 では使い方が少し変わっています。
import {BrowserRouter, Switch, Route} from "react-router-dom"
// 省略
const App = () => {
return(
<div className="App">
<BrowserRouter>
<Switch>
<Route>
<Sample1 />
</Route>
<Route>
<Sample2 />
</Route>
<Route>
<Sample3 />
</Route>
</Switch>
</BrowserRouter>
</div>
)
}
react-router ソースコード
今回読むのは、本家の react-router-dom からフォークしたプロジェクトです。使い方は v5 に似ています。本家は、こちら にあります。
全てのソースコードは、こちら にあり、Switch, Route は こちら に、BrowserRouter, Link は こちら にあります。
では、読んでみましょう!
BrowserRouter
ソースコードは、こちら にあります。
ここでは、history という履歴管理ができるライブラリの createBrowserHistory という機能を使い、history を導入しています。
// 省略
import { createBrowserHistory as createHistory } from "history";
// 省略
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
history の詳細は こちら。useHistory のように history.push(location) できたり、history.location で現在のURLを取得できたり、history.listen で history の変化に応じた 関数の実行を制御できたりします。
そんな history オブジェクトを作っているのが、上のコードの createBrowserHistory になります。
ここでは、その history を props にして、 に流しています。
では、Router を見てみましょう (ソースコードは こちら)。
// 省略
class Router extends React.Component {
// 省略
constructor(props) {
super(props);
this.state = {
location: props.history.location
};
// 省略
// history を listen して、history の変化に応じて state.location を変更している。
}
// コンポーネントの表示時に state.location をどうするか制御している。
render() {
return (
<RouterContext.Provider
children={this.props.children || null}
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}
/>
);
}
}
ここでは、
1. history の内容の変化を listen している state location の定義
2. RouterContext.Provider での history, location, match(現在のパスが "/" と一致しているか) の情報を、context として流す
の 2つをしています。
この RouterContext.Provider の内容が、RouterContext.Consumer で 取り出されます。
では、BrowserRouter のさらに下にある Switch を見てみましょう。
Switch
Switch は、Switch.js の中に書いてあります (ソースコードはこちら)。
// 省略
class Switch extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Switch> outside a <Router>");
const location = this.props.location || context.location;
let element, match;
// We use React.Children.forEach instead of React.Children.toArray().find()
// here because toArray adds keys to all child elements and we do not want
// to trigger an unmount/remount for two <Route>s that render the same
// component at different URLs.
React.Children.forEach(this.props.children, child => {
if (match == null && React.isValidElement(child)) {
element = child;
const path = child.props.path || child.props.from;
match = path
? matchPath(location.pathname, { ...child.props, path })
: context.match;
}
});
return match
? React.cloneElement(element, { location, computedMatch: match })
: null;
}}
</RouterContext.Consumer>
);
}
}
ここでは、
1. location を props か context から取得
2. 子要素 ( Route ) の path もしくは from の props が、1 で取得した location と match するか (matchPath) を確認し、match した場合のみ 子要素の中身を clone して表示
の 2つのことをしています。
ページ遷移の実態が、history の location と一致するかで、React.cloneElement か null か分岐させているだけなのが見てとれますね。
また、matchPath (ソースコードはこちら) では path-to-regexp というライブラリを使い、マッチするかを確認していますが、ここでは割愛します。
次に、Switch の中身の Route を見てみましょう。
Route
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Route> outside a <Router>");
const location = this.props.location || context.location;
const match = this.props.computedMatch
? this.props.computedMatch // <Switch> already computed the match for us
: this.props.path
? matchPath(location.pathname, this.props)
: context.match;
const props = { ...context, location, match };
let { children, component, render } = this.props;
// 省略
return (
<RouterContext.Provider value={props}>
{children && !isEmptyChildren(children)
? children
: props.match
? component
? React.createElement(component, props)
: render
? render(props)
: null
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}
ここでは、
1. context から location を取得
2. props か matchPath(前述) か context から、location と 現在のパスが match しているかを取得
3. match しているかで、表示(createElement か render)か非表示(null)を分岐
の3つをしています。
Link
// 省略
class Link extends React.Component {
handleClick(event, history) {
if (this.props.onClick) this.props.onClick(event);
// 省略
// 左クリックの場合、history.push か history.replace する
}
render() {
const { innerRef, replace, to, ...rest } = this.props; // eslint-disable-line no-unused-vars
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Link> outside a <Router>");
const location =
typeof to === "string"
? createLocation(to, null, null, context.location)
: to;
const href = location ? context.history.createHref(location) : "";
return (
<a
{...rest}
onClick={event => this.handleClick(event, context.history)}
href={href}
ref={innerRef}
/>
);
}}
</RouterContext.Consumer>
);
}
}
ここでやっているのは、下の prop を持つ a タグを作ることです。
Href : createLocation で history オブジェクトの to を作り、それを createHref で string 型の href にする。
onClick : prop で流れた onClick を実行する。左クリックの場合は、history.push か replace をする。
ref : props から取得した ref
ここまでで、v5 に近い react-router-dom の実装での、BrowserRouter, Switch, Route, Link の実装を見ました。