0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-01-29

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

対象読者

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

react-router とは?

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

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 に似ています。本家は、こちら にあります。

全てのソースコードは、こちら にあり、Switch, Route は こちら に、BrowserRouter, Link は こちら にあります。

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

BrowserRouter

ソースコードは、こちら にあります。

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

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 の詳細は こちら。useHistory のように history.push(location) できたり、history.location で現在のURLを取得できたり、history.listen で history の変化に応じた 関数の実行を制御できたりします。

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

では、Router を見てみましょう (ソースコードは こちら)。

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 の中に書いてあります (ソースコードはこちら)。

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

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

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 の実装を見ました。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?