30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

宣言的な React と、ミュータブルな this

宣言的と命令的の対比

React の特徴のひとつとして、UI を宣言的に記述する、というものが挙げられる。
これは、UI の最終結果だけを記述する、ということである。
データと UI が一対一で、このデータのときはこのような UI になる、という書き方をする。

これと対照的で、よく引き合いに出されるのが、命令的な記述。
これは、既存の UI に対して命令を繰り返すことで UI を作っていく。そのため、既存の UI の状態に依存し、それを考慮しながら記述しなければならない。
これが、宣言的な記述との大きな違い。宣言的な記述は作るべき成果物だけを意識すればいいが、命令的な記述はそれだけではなく、作るべき UI と既存の UI との差分を考慮し、それをコードに落とし込まないといけない。
宣言的に記述することで、経時的な変化を意識せずにコードを書けるようになり、コードが単純になり、保守性や可読性が高まる。

なぜ React を使うと宣言的に UI を記述できるのかというと、「作るべき UI と既存の UI との差分の考慮」という部分を React に丸投げできるからである。
開発者は、作るべき UI についてのみ記述すればよく、あとは React が差分検出アルゴリズムを使って、効率的に UI を書き換えてくれる。
もちろん常に最適な更新を行ってくれるわけではないし、パフォーマンスを高めるためには React の使い方に習熟する必要もある。だがそれはあくまで、「作るべき UI をより効率的に生成するにはどうすればいいのか」というのが論点であり、「時間軸」や「既存の UI との差分」といった概念からは解放される。

「React による UI の効率的な更新」を例示する。
これは、3秒毎にオンライン状態であるユーザーのリストを受け取り、それをidの順にソートした上でリスト形式で表示するアプリ。
getOnlineUsersAppは本質ではないので流し読みでよい。重要なのはOnlineUsers

// React v16.10.2 で動作確認している。この記事の他の React アプリのコードも同様
import React, {useEffect, useState} from 'react';

// ランダムな user のリストを返す
const getOnlineUsers = () => {
  const users = [
    {id: 1, name: 'Alice'},
    {id: 2, name: 'Bob'},
    {id: 3, name: 'Carol'},
    {id: 4, name: 'Dave'},
    {id: 5, name: 'Ellen'},
  ];

  const list = [];
  for (let i = 0; i <= 3; i += 1) {
    const index = Math.floor(Math.random() * users.length);
    if (!list.includes(index)) list.push(index);
  }

  return list.map(i => users[i]);
};

// 3秒毎に getOnlineUsers を実行し、その返り値を users に格納する
// users が更新される度に、それを OnlineUsers に渡している
const App = () => {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    const timerId = setInterval(() => {
      setUsers(getOnlineUsers());
    }, 3000);
    return () => clearInterval(timerId);
  }, []);

  return (
    <div>
      <OnlineUsers users={users} />
    </div>
  );
};

// 受け取った users を昇順でソートし、それをリスト形式で表示する
const OnlineUsers = ({users}) => {
  return (
    <ul>
      {users
        .sort((a, b) => a.id - b.id)
        .map(user => (
          <li key={user.id}>{`${user.id}: ${user.name}`}</li>
        ))}
    </ul>
  );
};

export default App;

OnlineUsersには、受け取ったusersをソートして、そしてそれをリスト形式で表示するという、「どのような UI を作るのか」という情報しか記述されていない。前回の UI(この例では3秒前の UI)がどのような状態であるか、については一切考慮していない。
にも関わらず、リストを丸ごと更新するのではなく、変更があった要素についてのみ更新が行われ、UI が効率的に再構築されている。

f:id:numb_86:20191123181845g:plain

同様のことを React のようなライブラリなしでやろうとすると、命令的な記述にならざるを得ず、コードが肥大化、複雑化していく。
単に行数が長くなるだけではなく、コードそのものが処理の内容を時系列で記述していくような形になりがちで、コードから UI の完成物を推察しづらくなってしまう。
次のコードはそのことを例示するためのサンプルに過ぎないので、興味がなければ読み飛ばして構わない。

// getOnlineUsers は先程と同じなので省略

setInterval(() => {
  const nextUsers = new Map(
      getOnlineUsers()
        .sort((a, b) => a.id - b.id)
        .map(user => [user.id, user])
    );
  const nextIds = Array.from(nextUsers.keys());

  const ulElement = document.querySelector('#online-users');
  const liElements = ulElement.querySelectorAll('li');
  const prevIds = [];
  liElements.forEach(item => {
    prevIds.push(Number(item.id.slice(item.id.length - 1)));
  });

  const appendIds = [];
  nextIds.forEach(id => {
    if(!prevIds.includes(id)) appendIds.push(id);
  });

  const removeIds = [];
  prevIds.forEach(id => {
    if(!nextIds.includes(id)) removeIds.push(id);
  });

  removeIds.forEach(id => {
    const elem = ulElement.querySelector(`#user-${id}`);
    elem.parentNode.removeChild(elem);
  });

  const existElementIds = [];

  nextIds.forEach(id => {
    if (!appendIds.includes(id)) {
      existElementIds.push(id);
      return;
    }

    const elem = document.createElement('li');
    elem.setAttribute('id', `user-${id}`)
    elem.textContent = `${id}: ${nextUsers.get(id).name}`;

    if (existElementIds.length === 0) {
      ulElement.insertBefore(elem, ulElement.firstChild);
      existElementIds.push(id);
      return;
    }

    const target = ulElement.querySelectorAll('li')[existElementIds.length - 1];
    target.parentNode.insertBefore(elem, target.nextSibling);
    existElementIds.push(id);
  });
}, 3000);

このような面倒な処理をライブラリに丸投げしてしまうことで、開発者がより本質的な作業に集中することでき、コードも簡潔になるのが、宣言的 UI の大きなメリットである。

データと UI の完全なシンクロ

ここまでは「見た目」の話をしてきたが、ウェブアプリ(だけではないが)における UI には、イベントハンドラも含まれる。
そしてイベントハンドラについても、宣言的に記述できる。
先程のOnlineUsersではpropsの内容に基づいて UI を作っていたので、今度はstateを使ってみる。

import React, {useState, Fragment} from 'react';

// `ms`ミリ秒だけ処理を停止させる関数
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

const candidates = ['Alice', 'Bob', 'Carol'];

const App = () => {
  const [target, setTarget] = useState('Alice');

  const selectTarget = e => {
    setTarget(e.currentTarget.value);
  };

  // 2秒後にログを表示する
  const vote = async () => {
    await sleep(2000);
    console.log(`${target} に投票しました`);
  };

  return (
    <div>
      {candidates.map(item => (
        <Fragment key={item}>
          <input
            type="radio"
            value={item}
            checked={item === target}
            onChange={selectTarget}
          />
          {`${item} `}
        </Fragment>
      ))}
      <br />
      <button type="button" onClick={vote}>
        投票する
      </button>
    </div>
  );
};

export default App;

投票したい人をチェックすると、targetという名前のstateにその人の名前が格納される。
「投票する」ボタンを押すと、ボタンを押したときにチェックしていた人に、投票される。
投票が行われるのはボタンを押した2秒後だが、その間に他の人にチェックしても、投票は正しく行われる。

f:id:numb_86:20191123181959g:plain

データと UI が一対一であり、両者がシンクロしているからこそ、このように挙動する。
この例でいうと、targetAliceのときはそれに対応した UI が作られ、targetBobのときはそれに対応した UI がまた新たに作られる。
イベントハンドラも UI の一部なので、vote関数はそれぞれ、次のような内容になる。

  // target が Alice のとき
  const vote = async () => {
    await sleep(2000);
    console.log(`Alice に投票しました`);
  };

  // target が Bob のとき
  const vote = async () => {
    await sleep(2000);
    console.log(`Bob に投票しました`);
  };

「投票する」ボタンを押したときのtargetAliceなので、ボタンのイベントハンドラに設定されているvoteは、2秒後にAlice に投票しましたと表示される。
このように、データ毎に UI が作られ、データが変わればまたそれに応じた UI が作られる。
そして新しい UI の内容で DOM を差し替えるのだが、既に見たようにそれは React が自動的かつ効率的に行なってくれる。
面倒なことは React がやってくれるので、難しいことを考えなくてもpropsstateなどのデータと UI は常に一対一になっており、シンクロしている。UI は必ずデータに基づいて構築され、その結びつきが破壊されることはない。

React 開発チームの一人である Dan Abramov 氏が、上手く表現している。

よく人は「すべては過程だ。結果ではない」と言います。ですが、 React の場合は逆です。全ては結果であり、過程ではありません。 これが jQuery の $.addClass と $.removeClass(過程)などの呼び出しと React であるべき CSS クラスを定義する行為(結果)の違いです。
React は現在の props と state に応じて DOM をシンクロします。 render 時は mount や update に区別はありません。
(中略)
初期 render か否かで違う挙動をするエフェクトを書こうとしてる場合は、React の流れに逆らっています! もし、結果が過程に頼ってしまっている場合は、シンクロに失敗しています。
props A, B, と C と順に render しようが C でいきなり render しようが関係ないはずです。

これが React の原則であり、それによってシンプルさと一貫性が生まれ、開発が非常に楽になる。

しかし React は内部に、この原則を崩しデータと UI のシンクロを破壊しかねない存在を抱えている。それが、クラスコンポーネントのthisである。

ミュータブルな this

propsstateが更新されれば UI も作り直される、というのが React の原則だが、クラスコンポーネントはその原則を破る。
一つの UI のなかで、propsstateが移り変わってしまう可能性がある。
先程の「投票ページ」をクラスコンポーネントに書き換えて、確認してみる。

import React, {Fragment} from 'react';

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

const candidates = ['Alice', 'Bob', 'Carol'];

class App extends React.Component {
  constructor() {
    super();
    this.state = {target: 'Alice'};

    this.selectTarget = this.selectTarget.bind(this);
    this.vote = this.vote.bind(this);
  }

  selectTarget(e) {
    this.setState({
      target: e.currentTarget.value,
    });
  }

  async vote() {
    await sleep(2000);
    console.log(`${this.state.target} に投票しました`);
  }

  render() {
    return (
      <div>
        {candidates.map(item => (
          <Fragment key={item}>
            <input
              type="radio"
              value={item}
              checked={item === this.state.target}
              onChange={this.selectTarget}
            />
            {`${item} `}
          </Fragment>
        ))}
        <br />
        <button type="button" onClick={this.vote}>
          投票する
        </button>
      </div>
    );
  }
}

export default App;

Aliceにチェックした状態で「投票する」ボタンを押下し、2秒以内にBobにチェックすると……。

f:id:numb_86:20191123182027g:plain

Bobに投票してしまう。

なぜこのようなことが起きるのかというと、thisがミュータブルであり、そしてクラスコンポーネントにおいてはpropsstatethisの一部だからである。
一つの UI のなかでもthisは変わり得るのであり、それゆえにthis.propsthis.stateも変わり得る。
今回はthis.state.targetAliceからBobに変わってしまった。

クラスコンポーネントのpropsstateが変化してしまうということを、今度は関数コンポーネントとの比較で見てみる。
次の例では、onClickというイベントハンドラを実行している途中で、this.propsが変化してしまう。

import React, {useEffect, useState} from 'react';

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

const App = () => {
  const [state, setState] = useState(0);

  useEffect(() => {
    const timerId = setInterval(() => {
      setState(s => s + 1);
    }, 1000);
    return () => clearInterval(timerId);
  }, []);

  return (
    <div>
      <h1>{state}</h1>
      <FunctionChild count={state} />
      <ClassChild count={state} />
    </div>
  );
};

const FunctionChild = props => {
  const onClick = async () => {
    console.log('==FUNCTION==');
    console.log('start count', props.count);
    await sleep(2000);
    console.log('finish count', props.count);
    console.log('==FUNCTION==');
  };

  return (
    <button type="button" onClick={onClick}>
      <b>FUNCTION</b>
    </button>
  );
};

class ClassChild extends React.Component {
  constructor(props) {
    super(props);
    this.onClick = this.onClick.bind(this);
  }

  async onClick() {
    console.log('==CLASS==');
    console.log('start count', this.props.count);
    await sleep(2000);
    console.log('finish count', this.props.count);
    console.log('==CLASS==');
  }

  render() {
    return (
      <button type="button" onClick={this.onClick}>
        <b>CLASS</b>
      </button>
    );
  }
}

export default App;

f:id:numb_86:20191123182052g:plain

関数コンポーネントではonClickのなかのprops.countはイミュータブルだが、クラスコンポーネントのthis.props.countonClickの処理の途中で値が変わってしまう。

  // count が 3 のとき

  const onClick = async () => {
    console.log('==FUNCTION==');
    console.log('start count', 3); // props.count は 3 を指す
    await sleep(2000);
    console.log('finish count', 3); // props.count は 3 を指す
    console.log('==FUNCTION==');
  };

  async onClick() {
    console.log('==CLASS==');
    console.log('start count', 3); // this.props.count は 3 を指す
    await sleep(2000);
    console.log('finish count', 5); // this.props.count は 3 を...指さない!
    console.log('==CLASS==');
  }

このため、クラスコンポーネントを使うと UI とデータのシンクロが崩れ、シンプルさや一貫性が失われ、thisの挙動を意識しながらコードを書かなければならなくなる。時に原則から逸脱することも必要かもしれないが、それはあくまでも開発者が自らの意思で逸脱するべきであり、知らぬ間に原則を壊しかねないthisの振る舞いは望ましくない。

まとめ

Hooks の利点や魅力については既に多くの言説が存在するが、個人的には、クラスコンポーネントを使う必要性がほとんど無くなりthisを扱わずに済むようになる、というのも大きいと思っている。
thisというミュータブルな値が入り込むと、イミュータブルなデータの内容に基づきそれに対応する UI を記述していけばよい、という宣言的 UI のメリットが薄れてしまう。
そして、JavaScript のthisはミュータブル性の他にも問題を抱えており、触らずに済むのならそれに越したことはない。

公式ドキュメントでも、「クラスは人間と機械の両方を混乱させる」として、クラスやthisが学習の障壁になっていると指摘、それが Hooks 導入の動機の一つでもあると述べている。
同時に「クラスコンポーネントは今後もサポートし続ける」とも述べており、無理にクラスコンポーネントを関数コンポーネントに置き換える必要はない。ただ、今後新しく作るコンポーネントについては、極力、関数コンポーネントにしたほうがよいのではないだろうか。

明日は、thisが抱えているもう一つの厄介な問題である「入れ子になった関数のなかだと値が変わってしまう」問題とその対処法について扱う。

参考資料

React Router v5.1 で導入された Hooks API について

React Router のv5.1で Hooks API が導入された。

reacttraining.com

URL パラメータやhistoryオブジェクトなどを、より簡単に取得できるようになった。
これまでの書き方も使えるが、今後のバージョンアップで非推奨になる可能性が高いとのことなので、今のうちに Hooks を使った書き方に慣れておいたほうがいいかもしれない。
個人的にはwithRouterを使わなくてもよくなるのが便利だと感じた。

この記事の内容は以下のバージョンで動作確認している。

  • react@16.10.2
  • react-router-dom@5.1.2

今回導入された API は以下の4つ。

  • useParams
  • useLocation
  • useHistory
  • useRouteMatch

コードを見ると、どれもReact.useContextを使って実装している模様。
react-router/hooks.js at v5.1.2 · ReactTraining/react-router

問題意識

上述のように、URL パラメータなどのルーティングに関する情報を取得する方法が、変わった。
「これまでと何が変わったのか」という視点で見たほうが分かりやすいので、まず従来の記法とその問題点を紹介することで、Hooks API が何を解決しているのかを説明する。

以下は、従来の記法で URL パラメータを取得している。
/component/xxxもしくは/render/xxxにアクセスすると、xxxの部分をpageIdとして認識して画面に表示する。

import React from 'react';
import {BrowserRouter, Switch, Route} from 'react-router-dom';

const Child = ({pageId}) => {
  return <p>This is child element in {pageId}</p>;
};

const ShowPageId = ({match}) => {
  return (
    <div>
      <p>Page id: {match.params.pageId}</p>
      <Child pageId={match.params.pageId} />
    </div>
  );
};

const App = () => (
  <BrowserRouter>
    <Switch>
      <Route path="/component/:pageId" component={ShowPageId} />
      <Route
        path="/render/:pageId"
        render={({match}) => <ShowPageId match={match} />}
      />
    </Switch>
  </BrowserRouter>
);
export default App;

従来は、ルーティングに関する情報をコンポーネントに渡すには、component属性かrender属性を使う必要があった。
だが、どちらも一長一短ある。
component属性を使うと、任意のpropsを渡すことが出来ない。render属性ならそれが可能だが、そうすると今度は、ルーティングに関する情報(上記の例ではmatch)も明示的に渡さなければならなくなる。
また、子要素(上記の例ではChild)にルーティングに関する情報を渡したいときは、その場合も明示的にpropsを渡さないといけない。

Hooks API はこれらの問題を解決する。

Hooks API ならルーティングに関する情報をどこからでも得られる

コンポーネントのなかでuseParamsを使えば、component属性やrender属性を使うことなく、URL パラメータを取得できる。

import React from 'react';
import {BrowserRouter, Switch, Route, useParams} from 'react-router-dom';

const Child = () => {
  const {pageId} = useParams();
  return <p>This is child element in {pageId}</p>;
};

const ShowPageId = ({originalProp}) => {
  const {pageId} = useParams();
  return (
    <div>
      <p>Page id: {pageId}</p>
      <p>{originalProp}</p>
      <Child />
    </div>
  );
};

const App = () => (
  <BrowserRouter>
    <Switch>
      <Route path="/component/:pageId">
        <ShowPageId originalProp="You can pass any parameter." />
      </Route>
    </Switch>
  </BrowserRouter>
);
export default App;

まず、Routeコンポーネントの書き方が変わる。component属性やrender属性で描画したいコンポーネントを指定するのではなく、Routeの子コンポーネントとして記述する。
そして当該コンポーネントのなかでuseParamsを使うことで、URL パラメータを取得できる。
ルーティングに関する情報を明示的にporpsとして渡す必要はないので、通常のコンポーネントとして扱える。任意のpropsを渡せるし、渡さなくてもよい。
useParamsを使えるのは、Route直下のコンポーネントだけではない。深い階層のコンポーネントであっても使える。上記の例では、ChilduseParamsを使っている。

useLocationuseHistoryも同じ要領で、locationオブジェクトとhistoryオブジェクトを取得するための API である。

import React from 'react';
import {
  BrowserRouter,
  Switch,
  Route,
  useLocation,
  useHistory,
} from 'react-router-dom';

const HooksSample = () => {
  const location = useLocation();
  const history = useHistory();
  console.log(Object.keys(location)); // ["pathname", "search", "hash", "state"]
  console.log(Object.keys(history)); // ["length", "action", "location", "createHref", "push", "replace", "go", "goBack", "goForward", "block", "listen"]
  return <div>Sample</div>;
};

const App = () => (
  <BrowserRouter>
    <Switch>
      <Route path="/component/:pageId">
        <HooksSample />
      </Route>
    </Switch>
  </BrowserRouter>
);
export default App;

なお、useHistoryは今後実装が予定されているuseNavigateの代用品のような位置付けらしい。今からuseHistoryを使っていくことで、同じ Hooks API であるuseNavigateに移行しやすくなる、ということだろう。

withRouter は非推奨になる可能性が高い

実は今までも、ルーティングに関する情報を明示的にコンポーネントに渡さなくても、withRouterを使えばそれを取得することが出来た。

import React from 'react';
import {BrowserRouter, Switch, Route, withRouter} from 'react-router-dom';

const ShowPageId = ({match}) => {
  return (
    <div>
      <p>Page id: {match.params.pageId}</p>
    </div>
  );
};
const WrappedShowPageId = withRouter(ShowPageId);

const App = () => (
  <BrowserRouter>
    <Switch>
      <Route path="/render/:pageId" render={() => <WrappedShowPageId />} />
    </Switch>
  </BrowserRouter>
);
export default App;

/render/xxxにアクセスすれば、URL パラメータを取得できていることを確認できる。

だが Hooks API が導入された今では、withRouterを使う積極的な理由はないはず。
しかも公式ブログによれば、今後のリリースで非推奨になる可能性が高いとのこと。

although withRouter is not deprecated in 5.1, it's a weird API when you can compose your state with hooks instead. It will also most likely be deprecated in a future release.

match オブジェクト等は同一性が保たれる

これは今回のバージョンアップで入ったものではなく以前からそうだったが、React Router から渡されるmatchオブジェクトなどは、コンポーネントがレンダリングされ直しても、同じ参照のオブジェクトが渡される。値が同じだけの別のオブジェクト、にはならない。

import React, {useState, useEffect} from 'react';
import {BrowserRouter, Switch, Route} from 'react-router-dom';

const paramsList = [];
const locationList = [];
const historyList = [];

const CountUp = props => {
  const [state, setState] = useState(0);

  const {match, location, history} = props;
  const {params} = match;

  useEffect(() => {
    paramsList.push(params);
    locationList.push(location);
    historyList.push(history);

    if (paramsList.length >= 2) {
      const prevIndex = paramsList.length - 2;
      const currentIndex = paramsList.length - 1;

      // いずれも常に true になる
      console.log(Object.is(paramsList[prevIndex], paramsList[currentIndex]));
      console.log(
        Object.is(locationList[prevIndex], locationList[currentIndex])
      );
      console.log(Object.is(historyList[prevIndex], historyList[currentIndex]));
    }
  });

  return (
    <div>
      <p>{state}</p>
      <button
        type="button"
        onClick={() => {
          setState(s => s + 1);
        }}
      >
        click
      </button>
    </div>
  );
};

const App = () => (
  <BrowserRouter>
    <Switch>
      <Route path="/component/:pageId" component={CountUp} />
    </Switch>
  </BrowserRouter>
);
export default App;

以下のように書き換えて Hooks API を使うようにしても、同じ結果になる。

@@ -1,15 +1,23 @@
 import React, {useState, useEffect} from 'react';
-import {BrowserRouter, Switch, Route} from 'react-router-dom';
+import {
+  BrowserRouter,
+  Switch,
+  Route,
+  useParams,
+  useLocation,
+  useHistory,
+} from 'react-router-dom';

 const paramsList = [];
 const locationList = [];
 const historyList = [];

-const CountUp = props => {
+const CountUp = () => {
   const [state, setState] = useState(0);

-  const {match, location, history} = props;
-  const {params} = match;
+  const params = useParams();
+  const location = useLocation();
+  const history = useHistory();

   useEffect(() => {
     paramsList.push(params);
@@ -47,7 +55,9 @@ const CountUp = props => {
 const App = () => (
   <BrowserRouter>
     <Switch>
-      <Route path="/component/:pageId" component={CountUp} />
+      <Route path="/component/:pageId">
+        <CountUp />
+      </Route>
     </Switch>
   </BrowserRouter>
 );

そのためmatchオブジェクトなどは、そのままでuseEffectdepsなどに使うことが出来る。

useRouteMatch

useRouteMatchは他の Hooks API とは使い方が異なり、Routeコンポーネントの代わりに使う。

以下のコードでは、Routeコンポーネントを使ってAdminMenuの表示を制御している。
例えば/user/1にアクセスしたときは共通メニューしか表示されないが、admin/1にアクセスすると管理者限定メニューも表示される。

import React from 'react';
import {BrowserRouter, Route} from 'react-router-dom';

const AdminMenu = ({match}) => {
  return (
    <>
      <h2>管理者限定メニュー</h2>
      <p>管理者ID: {match.params.adminId}</p>
      <ul>
        <li>ユーザー追加</li>
        <li>ユーザー削除</li>
      </ul>
    </>
  );
};

const Menu = () => {
  return (
    <div>
      <h1>Menu</h1>
      <h2>共通メニュー</h2>
      <ul>
        <li>データ閲覧</li>
        <li>データ編集</li>
      </ul>
      <Route path="/admin/:adminId" component={AdminMenu} />
    </div>
  );
};

const App = () => (
  <BrowserRouter>
    <Menu />
  </BrowserRouter>
);
export default App;

同じことをuseRouteMatchを使って実装したのが以下。

@@ -1,5 +1,5 @@
 import React from 'react';
-import {BrowserRouter, Route} from 'react-router-dom';
+import {BrowserRouter, useRouteMatch} from 'react-router-dom';

 const AdminMenu = ({match}) => {
   return (
@@ -15,6 +15,9 @@ const AdminMenu = ({match}) => {
 };

 const Menu = () => {
+  const match = useRouteMatch({
+    path: '/admin/:adminId',
+  });
   return (
     <div>
       <h1>Menu</h1>
@@ -23,7 +26,7 @@ const Menu = () => {
         <li>データ閲覧</li>
         <li>データ編集</li>
       </ul>
-      <Route path="/admin/:adminId" component={AdminMenu} />
+      {match && <AdminMenu match={match} />}
     </div>
   );
 };

v6 への準備

v5.1では、これまでの書き方も問題なく使える。だが今後のリリースで非推奨になる可能性が高い。
Hooks API を積極的に使うことは、今後のメジャーバージョンアップへの準備になる。

Although they are not deprecated in 5.1, the and APIs have several quirks that just aren't needed (see the discussion in useParams above) in a world with hooks. We will most likely deprecate these APIs in a future release.

両方の書き方を使えるv5.1で漸進的に準備を進めておくと、よいかもしれない。

参考資料