react-router@v6で何が変わるのか

まさかこの「誰よりも先駆けて使い方を解説！」な記事を書いてから1年半もかかると思ってなかったのですが(笑)、ついにv6出ましたね。
https://github.com/remix-run/react-router/releases/tag/v6.0.0

# はじめに

ReactでSPAを作るときのおそらくデファクトスタンダードであろうreact-routerですが現在v6が開発中で、しばらくしたら単純に`npm install react-router`とするとv6がインストールされるようになります。
v6はAPIに破壊的な変更が入ります。つまり、今ある入門記事の通りに書いても動かなくなります。
というわけでこの記事では書き方がどう変わるか、そして個人的により重要な中身がどう変わったのかについて説明します。

# 第1部：書き方の変更編

## v5での書き方

まずは前提知識としてv5での書き方です[^1]。すでに優良な記事があるので詳しくは解説しません。

[^1]: react-router公式のQuick Startより引用。リンク張ってもそのうち内容変わりそうですが一応リンク：https://reacttraining.com/react-router/web/guides/quick-start

```jsx:v5での書き方
<Router>
  <Switch>
    <Route path="/about">
      <About />
    </Route>
    <Route path="/users">
      <Users />
    </Route>
    <Route path="/">
      <Home />
    </Route>
  </Switch>
</Router>
```

`Router`の中に`Route`を書き、パスとマッチしたときにレンダリングされる要素を指定します。
`Route`を`Switch`で囲んだ場合は「初めにマッチしたもの」がレンダリングされる、囲まない場合は「マッチするものが全部」レンダリングされます。

なお`Route`でレンダリングされる要素を指定する方法は以下の**4種類**があります。

- `children`、つまり、`Route`の中に書く（推奨）
- `component`で指定する[^2]
- `render`で「要素を返す関数」を渡す
- `children`で関数を渡す。1つ目との違いはpropである点。またrenderとの違いは「マッチしたかに関わらず常にレンダリングされる」点

[^2]: 前にQuick Start見たときはこれで書いてあった気がするのだけど記憶違いかな

普通は「普通のchildren」もしくはcomponentを使うと思いますがはっきり言ってややこしいです。

## v6ではこうなる

現時点（2020/4/4）でv6を試してみるには以下のようにインストールします。
[リリースノート](https://github.com/ReactTraining/react-router/releases/tag/v6.0.0-alpha.2)だと`@6`みたいに書かれていましたがそれだと「そんなバージョンない」エラーになりました[^3]。

[^3]: `6.0.0-alpha.2`のように正確に指定すればインストールできます。

```
npm install history@next react-router@next react-router-dom@next
```

2020/4/4（早朝）時点で入るバージョンは以下となります。

```
+ react-router-dom@6.0.0-alpha.2
+ react-router@6.0.0-alpha.2
+ history@5.0.0-beta.7
```

さて、v6では上で示したルーティングの書き方が以下のようになります。

```jsx:v6での書き方
<Router>
  <Routes>
    <Route path="/about" element={<About />} />
    <Route path="/users" element={<Users />} />
    <Route path="/" element={<Home />} />
  </Routes>
</Router>
```

- `Switch`はなくなり`Routes`になりました。なおv5では`Switch`は「なくてもいい（直接`Route`を書いてもいい）」でしたが、v6では**必ず`Routes`で囲む必要があります**
- レンダリングされる要素は`element`で指定する。**これ以外の方法はない**

`element`で指定する方法はv5での「普通のchildren」での指定方法と同じです。まあちょっと/が多いのが気になりますが。

[Getting Started](https://github.com/ReactTraining/react-router/blob/v6.0.0-alpha.2/docs/installation/getting-started.md)によるとv6では単に書き方が変わるだけでなく以下の機能が追加されています。

- `Route`に`Route`をネストできるようになった。これは相対パスとして動作します
- `Link`も相対パスで書けるようになった。これは[migration guide](https://github.com/ReactTraining/react-router/blob/v6.0.0-alpha.2/docs/advanced-guides/migrating-5-to-6.md#relative-routes-and-links)のbefore(v5) after(v6)を見るととてもシンプルですね

他に注意が必要な点としては、v5ではパスパラメータ（`path=/users/:id`みたいに指定するやつ）を取得する方法が**`useParams`関数のみになります。propsでmatchは渡されなくなります**。

# 第2部：内部構造の変更編

さて（個人的）本題はここからです。

## v5の実装

そもそもこの記事を書くきっかけとなったのはreact-routerがどう動いているかを調べようと思ったことでした。
react-router@v5を使ってるプログラムをReact Developer Toolsで見ると以下のようになります。非常にややこしい。

![react-router@v5_1](https://user-images.githubusercontent.com/1846465/78410601-5ebeea80-75fc-11ea-9e54-2b85c1379c56.jpg)

### RouterとRouterContext

`Route`のような表示対象に依存しないコードは[react-router](https://github.com/ReactTraining/react-router/tree/v5.1.2/packages/react-router/modules)に、`Link`のように表示対象に依存するコードは[react-router-dom](https://github.com/ReactTraining/react-router/tree/v5.1.2/packages/react-router-dom/modules)に置かれています。リンク張るバージョンは5.1.2です。

`BrowserRouter`は後で見るので、まずreact-routerパッケージの方の[Router.js](https://github.com/ReactTraining/react-router/blob/v5.1.2/packages/react-router/modules/Router.js)を見ます。するとContextを使っていることがわかります。

```js:Router.js抜粋
import RouterContext from "./RouterContext";
```

この[`RouterContext`](https://github.com/ReactTraining/react-router/blob/v5.1.2/packages/react-router/modules/RouterContext.js)はReact 16で導入されたコンテキストではありません。後々関わってくるのは1行目のコメントです。

```js:RouterContext.js
// TODO: Replace with React.createContext once we can assume React 16+
import createContext from "mini-create-react-context";

const createNamedContext = name => {
  const context = createContext();
  context.displayName = name;

  return context;
};

const context = /*#__PURE__*/ createNamedContext("Router");
export default context;
```

ともかくこのContextを使ってレンダリングが行われています。`history`と`location`は後で見ます。

```js:Router.js抜粋
  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
        }}
      />
    );
  }
```

### Link

次に`Link`（がレンダリングしてるaタグ）がクリックされたときに何が起こるか[Link.js](https://github.com/ReactTraining/react-router/blob/v5.1.2/packages/react-router-dom/modules/Link.js)を見てみましょう。`Link`と`LinkAnchor`に分かれててややこしいですが[^4]全体として以下のように動作します（長くなるのでコードは貼りません。リンク先を見ながら説明を読んでください）

[^4]: 分かれてる理由は「リンク」としてレンダリングされる要素を`component`として渡せるからのようですがドキュメントには特にその旨ないですね

1. `Link`：Contextとして渡される`history`を操作する`navigate`関数を定義し`LinkAnchor`に渡す
2. `LinkAnchor`：aタグがクリックされたら渡された`navigate`関数を実行する

これでクリックされたらhistory（ブラウザ履歴）が変わるところはできました。

### 再びRouter

ここまでコードを読んで、「ブラウザ履歴が変わるのはわかったけど、React的にはどうやってレンダリングし直してるの？」ということはわからなかったのでもう一度[Router.js](https://github.com/ReactTraining/react-router/blob/v5.1.2/packages/react-router/modules/Router.js)を見てみました。

```js:Router.js抜粋
  constructor(props) {
    super(props);

    this.state = {
      location: props.history.location
    };

    // This is a bit of a hack. We have to start listening for location
    // changes here in the constructor in case there are any <Redirect>s
    // on the initial render. If there are, they will replace/push when
    // they mount and since cDM fires in children before parents, we may
    // get a new location before the <Router> is mounted.
    this._isMounted = false;
    this._pendingLocation = null;

    if (!props.staticContext) {
      this.unlisten = props.history.listen(location => {
        if (this._isMounted) {
          this.setState({ location });
        } else {
          this._pendingLocation = location;
        }
      });
    }
  }
```

最後のif文は普通の使い方であれば実行されます。つまり、下の方で`history`の操作を行えばそれが`Router`に通知され、stateを変え、再レンダリングされるという仕組みのようです。

### BrowserRouter

後回しにしていた`BrowserRouter`です。特に難しいことはしていません。`history`オブジェクトを作成して`Router`に渡しています。
historyパッケージはreact-routerと同じ開発チームが作っているようです。

```js:BrowserRoter.js抜粋
import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";

/**
 * The public API for a <Router> that uses HTML5 history.
 */
class BrowserRouter extends React.Component {
  history = createHistory(this.props);

  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}
```

### Route

v5実装巡りの最後に[Route.js](https://github.com/ReactTraining/react-router/blob/v5.1.2/packages/react-router/modules/Route.js)を見てみましょう。この記事を書くきっかけになったコードです。

```js:Route.js抜粋
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          // 省略

          return (
            <RouterContext.Provider value={props}>
              {props.match
                ? children
                  ? typeof children === "function"
                    ? __DEV__
                      ? evalChildrenDev(children, props, this.props.path)
                      : children(props)
                    : children
                  : component
                  ? React.createElement(component, props)
                  : render
                  ? render(props)
                  : null
                : typeof children === "function"
                ? __DEV__
                  ? evalChildrenDev(children, props, this.props.path)
                  : children(props)
                : null}
            </RouterContext.Provider>
          );
        }}
      </RouterContext.Consumer>
    );
  }
```

(　ﾟдﾟ)
三項演算子をここまで使いまくったコードは初めて見ました。
このコードがわかりにくいのは中盤のインデントが「わかりにくい」ためです。インデントを「直す」と以下のようになります。

```js
{props.match
  ? children
    ? typeof children === "function"
      ? __DEV__
        ? evalChildrenDev(children, props, this.props.path)
        : children(props)
      : children
    : component // childがfalsyの場合
      ? React.createElement(component, props) // child:fasly, component:truthy
      : render // childもcomponentもfalsyの場合
        ? render(props) // child:fasly, component:falsy, render:truthy
        : null
  : typeof children === "function"
    ? __DEV__
      ? evalChildrenDev(children, props, this.props.path)
      : children(props)
    : null}
```

いやあんまり変わらねえよ。わかりにくい。
何これ直してpullreq送るべきなの？と思ったら[すでにされており](https://github.com/ReactTraining/react-router/pull/7220)、さらに衝撃的な事実が発覚しました。

> I've stated before that this form is more readable, but I know Ryan and Michael disagree.
訳：前にこっちの方が読みやすいって言ったんだけど、作者が反対したんだ。

まじですか。
ここで第1部で触れたレンダリングされる要素を指定する4つの方法を振り返りましょう。

- `children`、つまり、`Route`の中に書く（推奨）
- `component`で指定する
- `render`で「要素を返す関数」を渡す
- `children`で関数を渡す。1つ目との違いはpropである点。またrenderとの違いは「マッチしたかに関わらず常にレンダリングされる」点

まあ確かに`children`が多義な点を除けば（これがいけなかったんじゃ）、APIドキュメントに書いてある順に処理されているので「わかりやすい」気がする。

## v6の実装

次にv6実装について見ていきましょう。
まずはv5との比較としてReact Developer Toolsでのコンポーネント階層です。v5からだいぶシンプルになっていますね。

![react-router@v6_1](https://user-images.githubusercontent.com/1846465/78422497-3a402e00-764f-11ea-8888-e1cae1262b99.jpg)

### ファイル構成の変更

v6でも`react-router`と`react-router-dom`にパッケージが分かれていることは変わりありません。
一方、ファイル構成は非常にシンプルになっています。リンク張るバージョンは[6.0.0-alpha.2](https://github.com/ReactTraining/react-router/tree/v6.0.0-alpha.2/packages)です[^5]。アルファなので以下で説明する実装は変わる可能性がありますが大きく変わることはないでしょう。

[^5]: 朝のうちにv5実装についてまで書き上げて、桜見物して帰ってきたら6.0.0-alpha.3がリリースされてましたｗ。主な違いは`Redirect`をなくしたことのようですね

[react-router](https://github.com/ReactTraining/react-router/tree/v6.0.0-alpha.2/packages/react-router)内に入ると「あれ？」ってぐらいシンプルになっています。本体のファイルはindex.jsだけです（react-router-domも同様）

![react-router@v6_2](https://user-images.githubusercontent.com/1846465/78422718-e20a2b80-7650-11ea-9fd5-2163f3f2c702.jpg)

### RouterとContext

ともかく[react-routerの方のindex.js](https://github.com/ReactTraining/react-router/blob/v6.0.0-alpha.2/packages/react-router/index.js)を見てみるとシンプルになった理由がわかります。
伏線しておきましたね？そう、v6はReactの[コンテキスト](https://ja.reactjs.org/docs/context.html)と[フック](https://ja.reactjs.org/docs/hooks-overview.html)を使った実装にリライトされています。

```js:react-router/index.js抜粋
const LocationContext = React.createContext();

export function Router({ children = null, history, timeout = 2000 }) {
  let [location, setLocation] = React.useState(history.location);
  let [startTransition, pending] = useTransition({ timeoutMs: timeout });
  let listeningRef = React.useRef(false);

  if (!listeningRef.current) {
    listeningRef.current = true;
    history.listen(({ location }) => {
      startTransition(() => {
        setLocation(location);
      });
    });
  }

  return (
    <LocationContext.Provider
      children={children}
      value={{ history, location, pending }}
    />
  );
}
```

初め見たときは「え？`LocationContext`グローバルでいいの？」と思いましたが、`Router`を複数使うなんてことはまずないでしょうし実装がシンプルになるのでいいと思います。

なお先に説明しておくと、
`Link`がレンダリングするaタグがクリックされることで`history`が操作される
→上記の`listen`で登録されているコールバックが実行される
→`setLocation`を使用して`location`を更新
→再レンダリング
という流れはv5と違いはありません（クラスとフックという違いを除けば）

### LinkとuseNavigate

`Link`は[react-router-domの方のindex.js](https://github.com/ReactTraining/react-router/blob/v6.0.0-alpha.3/packages/react-router-dom/index.js)に書かれています。

```js:react-router-dom/index.js抜粋
export const Link = React.forwardRef(function LinkWithRef(
  {
    as: Component = 'a',
    // 省略
    to,
    ...rest
  },
  ref
) {
  let href = useHref(to);
  let navigate = useNavigate();
  let location = useLocation();
  let toLocation = useResolvedLocation(to);

  function handleClick(event) {
    if (onClick) onClick(event);
    if (
      // 省略。普通の使い方であれば成立します
    ) {
      // 省略

      navigate(to, { replace, state });
    }
  }

  return (
    <Component
      {...rest}
      href={href}
      onClick={handleClick}
      ref={ref}
      target={target}
    />
  );
});
```

`useHref`～`useResolveLocation`はreact-routerパッケージで定義されているフックです。次に[`useNavigate`](https://github.com/ReactTraining/react-router/blob/v6.0.0-alpha.2/packages/react-router/index.js#L378-L412)に移りましょう。

```js:react-router/index.js抜粋
export function useNavigate() {
  let { history, pending } = React.useContext(LocationContext);
  let { pathname } = React.useContext(RouteContext);

  let navigate = React.useCallback(
    (to, { replace, state } = {}) => {
      if (typeof to === 'number') {
        history.go(to);
      } else {
        let relativeTo = resolveLocation(to, pathname);

        // If we are pending transition, use REPLACE instead of PUSH.
        // This will prevent URLs that we started navigating to but
        // never fully loaded from appearing in the history stack.
        let method = !!replace || pending ? 'replace' : 'push';
        history[method](relativeTo, state);
      }
    },
    [history, pending, pathname]
  );

  return navigate;
}
```

第1部で紹介した相対パスの処理も行われていますが基本的にはv5のころに`Link`コンポーネント内で行われていた処理が抽出されています。その分`Link`の方はシンプルになりました。

### RouteとRoutes

`BrowserRouter`は特筆することないので飛ばして、v5実装で魔窟化していた[`Route`](https://github.com/ReactTraining/react-router/blob/v6.0.0-alpha.2/packages/react-router/index.js#L164-L169)に移ります。

```js:react-router/index.js抜粋
export function Route({ element }) {
  return element;
}
```

これまた見るところ間違えたかなと思うぐらいシンプルです。が、第1部で見たようにv6ではRouterは`element`しか受け付けないのでこれであってます。~~やはりv5の仕様が単に狂ってただけじゃ~~

複雑な実装、ってよく思うとv5実装解説ではそこら辺さくっと略しましたが、はどこに行ったかというと[`Routes`](https://github.com/ReactTraining/react-router/blob/v6.0.0-alpha.2/packages/react-router/index.js#L232-L239)です。
より正確には、

1. [`createRoutesFromChildren`](https://github.com/ReactTraining/react-router/blob/v6.0.0-alpha.2/packages/react-router/index.js#L250-L291)でchildrenの情報を集めて
2. [`useRoutes`](https://github.com/ReactTraining/react-router/blob/v6.0.0-alpha.2/packages/react-router/index.js#L450-L538)（内部で[`matchRoutes`](https://github.com/ReactTraining/react-router/blob/v6.0.0-alpha.2/packages/react-router/index.js#L548-L609)呼び出し）を使って`location`に対応する`Route`をレンダリング

ということが行われています。ここの部分については淡々とがんばってるだけなので省略。

# あとがき

以上、react-router@v5→v6での使い方の変更、内部実装の変更について見てきました。
途中でも書いたように当初はv5の実装理解が目的、そこで見かけた「超絶三項演算子利用例」についてだけ書こうかと思ったのですが、「いや待てそういえばリリースの方見るとそろそろv6出るよな」と見に行ったらフックを使う実装に置き換わっていた（うえに`Route`もシンプル化して超絶三項演算子もなくなった）のでこちらも実装解説、さらにもう少し一般向け（？）に「使い方がどう変わるのか」についても書くことにしました。破壊的なAPI変更はOh...ですけど、おそらく売りとなる相対パスはかなり有用そうだと思いました。

ちなみに、フック利用＆シンプル化の効果は出ており、v6はかなり「軽く」なっています。
[海外の記事](https://dev.to/emreloper/react-router-v6-in-two-minutes-2i96)だと9.4kbが2.9kbになった（gzippedされてる場合）と書かれていますが、[公式ドキュメント](https://github.com/ReactTraining/react-router/blob/v6.0.0-alpha.2/docs/installation/add-to-a-website.md)ではCDN指定としてreact-routerとreact-router-domが分かれているのでその指定で正しいのか？というのが疑問（v5はreact-router-domだけで正しい）、でも[unpkg](https://unpkg.com/browse/react-router-dom@6.0.0-alpha.2/umd/)見るとreact-router-dom単体はminifiedのサイズ8.5kbもないしとやや根拠が怪しいですが軽くはなっているようです。