react-router のソースコードを読んでみる

react-router を読んでみたので、そのメモ。

## 対象読者

一回は、react-router を使ったことがある人。

## react-router とは？

React のページ遷移を管理するライブラリ。
使い方は、下のように、BrowserRouter と Switch で囲った部分の 各 Route が、ページになる (v5)。
最新の v6 では使い方が少し変わっています。

https://qiita.com/Ikko_Kojima/items/ab1fa2677fd7b5707dd5

```javascript:App.tsx
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 に似ています。本家は、[こちら](https://github.com/remix-run/react-router) にあります。

全てのソースコードは、[こちら](https://github.com/staylor/react-router) にあり、Switch, Route は [こちら](https://github.com/staylor/react-router/tree/master/packages/react-router/modules) に、BrowserRouter, Link は [こちら](https://github.com/staylor/react-router/tree/master/packages/react-router-dom/modules) にあります。

では、読んでみましょう！

## BrowserRouter

ソースコードは、[こちら](https://github.com/staylor/react-router/blob/master/packages/react-router-dom/modules/BrowserRouter.js) にあります。

ここでは、history という履歴管理ができるライブラリの createBrowserHistory という機能を使い、history を導入しています。

```javascript:BrowserRouter.js
// 省略
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 の詳細は [こちら](https://zenn.dev/stin/articles/how-to-develop-react-router#history-ライブラリについて)。useHistory のように history.push(location) できたり、history.location で現在のURLを取得できたり、history.listen で history の変化に応じた 関数の実行を制御できたりします。

そんな history オブジェクトを作っているのが、上のコードの createBrowserHistory になります。
ここでは、その history を props にして、 <Router> に流しています。

では、Router を見てみましょう (ソースコードは [こちら](https://github.com/staylor/react-router/blob/master/packages/react-router/modules/Router.js))。

```javascript:Router.js
// 省略
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 の中に書いてあります (ソースコードは[こちら](https://github.com/staylor/react-router/blob/master/packages/react-router/modules/Switch.js))。

```javascript: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 (ソースコードは[こちら](https://github.com/staylor/react-router/blob/master/packages/react-router/modules/matchPath.js)) では path-to-regexp というライブラリを使い、マッチするかを確認していますが、ここでは割愛します。

次に、Switch の中身の Route を見てみましょう。

## Route

```javascript:Route.js
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

```javascript:Link.js
// 省略
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 の実装を見ました。