【速報】React 18 の新機能 startTransition を試してみた

# この記事について

次期 React 18 の概要が[発表されました](https://reactjs.org/blog/2021/06/08/the-plan-for-react-18.html)。この記事では、React 18 の新機能である [startTransition](https://github.com/reactwg/react-18/discussions/41) をについて理解を深めたいと思います。

# まずは startTransition を使わない場合

いきなりですが、startTransition を使わない場合のソースです

```test1.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@18.0.0-alpha-e6be2d531/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18.0.0-alpha-e6be2d531/umd/react-dom.production.min.js"></script>

<script type="module">

  const sleep = msec => {
    const start = new Date().getTime();
    while (new Date().getTime() <= start + msec);
  }

  const Test = props => {
    
    const [state1, setState1] = React.useState(new Date().getTime());
    const [state2, setState2] = React.useState(new Date().getTime());
    
    return React.createElement(
      React.Fragment, {}, [
        React.createElement("p", {}, "Test New Transition"),
        React.createElement("button", {
          onClick: () => {

            setState1(new Date().getTime());
            
            setState2(new Date().getTime());

          }
        }, "Click!"),
        React.createElement("p", {}, state1),
        React.createElement(TestChild1, {caption: state2}),
      ]
    );
  }

  const TestChild1 = props => {
    const c = [];
    for (let i = 0; i < 100; i++) {
      c.push(React.createElement(TestChild2, {caption: `${i}: ${props.caption}`}));
    }
    return React.createElement("p", {}, c);
  }

  const TestChild2 = props => {
    sleep(10); // 重い処理
    return React.createElement("p", {}, props.caption);
  }

  const root = ReactDOM.createRoot(document.getElementById("App"));
  root.render(React.createElement(Test));

</script>
```

ずぼらによるずぼらのためのソースです。必要なのはお気に入りのエディタとお気に入りのブラウザ(IEを除く)だけです。「React は難しい」「React は面倒くさい」と誤解をされたくないので、僕は多くの場合この形式でソースを提示しています。すでに React をバリバリ使いこなしている方には鬱陶しいかもしれませんが、僕はこのスタイルを貫き続けるつもりです。

`sleep()` 関数は、重い処理を疑似的に実現するための関数です。
TestChild2 コンポーネントで、レンダリングのたびに 10ms の重い処理が発生します。そして、TestChild1 コンポーネントは、レンダリングの度に 100 個の TestChild1 コンポーネントを作成しますので、ボタンをクリックする度に 10ms x 100、すなわち計 1 秒のレンダリング時間がかかることになり、1 秒間はボタンをクリックできないことになります。

# さて startTransition を使ってみるか・・・
期待に胸を膨らませながら、startTransition を導入します！

```test2.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@18.0.0-alpha-e6be2d531/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18.0.0-alpha-e6be2d531/umd/react-dom.production.min.js"></script>

<script type="module">

  const sleep = msec => {
    const start = new Date().getTime();
    while (new Date().getTime() <= start + msec);
  }

  const Test = props => {
    
    const [state1, setState1] = React.useState(new Date().getTime());
    const [state2, setState2] = React.useState(new Date().getTime());
    
    return React.createElement(
      React.Fragment, {}, [
        React.createElement("p", {}, "Test New Transition"),
        React.createElement("button", {
          onClick: () => {

            setState1(new Date().getTime());
            
            React.startTransition(() => { // ★★★ここを変更★★★
              setState2(new Date().getTime());
            });

          }
        }, "Click!"),
        React.createElement("p", {}, state1),
        React.createElement(TestChild1, {caption: state2}),
      ]
    );
  }

  const TestChild1 = props => {
    const c = [];
    for (let i = 0; i < 100; i++) {
      c.push(React.createElement(TestChild2, {caption: `${i}: ${props.caption}`}));
    }
    return React.createElement("p", {}, c);
  }

  const TestChild2 = props => {
    sleep(10); // 重い処理
    return React.createElement("p", {}, props.caption);
  }

  const root = ReactDOM.createRoot(document.getElementById("App"));
  root.render(React.createElement(Test));

</script>
```

`setState2()`を startTransision で囲ってみました・・・が、ボタンはやはり１秒間押せないままです。ただ、state1 と state2 を描画するタイミングが、test1.html と test2.html では異なるようです。どういうことなのでしょうか。

# 描画タイミングを調べる

test2.html では、描画が２回起こっているようですので確認してみます。

```test3.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@18.0.0-alpha-e6be2d531/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18.0.0-alpha-e6be2d531/umd/react-dom.production.min.js"></script>

<script type="module">

  const sleep = msec => {
    const start = new Date().getTime();
    while (new Date().getTime() <= start + msec);
  }

  const Test = props => {
    
    const [state1, setState1] = React.useState(new Date().getTime());
    const [state2, setState2] = React.useState(new Date().getTime());
    
    return React.createElement(
      React.Fragment, {}, [
        React.createElement("p", {}, "Test New Transition"),
        React.createElement("button", {
          onClick: () => {

            setState1(new Date().getTime());
            
            React.startTransition(() => {
              setState2(new Date().getTime());
            });

          }
        }, "Click!"),
        React.createElement("p", {}, state1),
        React.createElement(TestChild1, {caption: state2}),
      ]
    );
  }

  const TestChild1 = props => {
    console.log(props.caption); // ★★★ここを変更★★★
    const c = [];
    for (let i = 0; i < 100; i++) {
      c.push(React.createElement(TestChild2, {caption: `${i}: ${props.caption}`}));
    }
    return React.createElement("p", {}, c);
  }

  const TestChild2 = props => {
    sleep(10); // 重い処理
    return React.createElement("p", {}, props.caption);
  }

  const root = ReactDOM.createRoot(document.getElementById("App"));
  root.render(React.createElement(Test));

</script>
```

どうも、`setState1()` で１回、`setState2()` で１回と、計２回レンダリングされているみたいですね。`setState1()` による再レンダリングの時は、state2 の内容は変化しないようなので、useMemo などを用いて重い処理を避けるようにすればよさそうです。

# 完成

完成したソースです。ボタンが繰り返しクリックできるようになりました！

```test4.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@18.0.0-alpha-e6be2d531/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18.0.0-alpha-e6be2d531/umd/react-dom.production.min.js"></script>

<script type="module">

  const sleep = msec => {
    const start = new Date().getTime();
    while (new Date().getTime() <= start + msec);
  }

  const Test = props => {
    
    const [state1, setState1] = React.useState(new Date().getTime());
    const [state2, setState2] = React.useState(new Date().getTime());
    
    return React.createElement(
      React.Fragment, {}, [
        React.createElement("p", {}, "Test New Transition"),
        React.createElement("button", {
          onClick: () => {

            setState1(new Date().getTime());
            
            React.startTransition(() => {
              setState2(new Date().getTime());
            });

          }
        }, "Click!"),
        React.createElement("p", {}, state1),
        React.createElement(TestChild1, {caption: state2}),
      ]
    );
  }

  const TestChild1 = props => {
    console.log(props.caption);
    const c = [];
    for (let i = 0; i < 100; i++) {
      c.push(React.createElement(TestChild2, {caption: `${i}: ${props.caption}`}));
    }
    return React.createElement("p", {}, c);
  }

  const TestChild2 = props => {
    React.useMemo(() => { // ★★★ここを変更★★★
      sleep(10); // 重い処理
    }, [props.caption]);
    return React.createElement("p", {}, props.caption);
  }

  const root = ReactDOM.createRoot(document.getElementById("App"));
  root.render(React.createElement(Test));

</script>
```

# 分かったこと

1. startTransition は、理解せずただやみくもに使っても意味がない。
1. 重い処理は、ユーザーの入力を阻害するほど重いものであってはならない。すなわち、適度にスライスされた処理でなければならない。同期処理ではうまくスライスできないくらいに重すぎる処理は Promise 化、すなわち Suspense を用いてスライスしてあげる必要がある。
1. useMemo などのメモ化機能がいよいよその本領を発揮してきた。

# おまけ

 useTransition も同じように試してみました。素敵ですね。

```test5.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@18.0.0-alpha-e6be2d531/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18.0.0-alpha-e6be2d531/umd/react-dom.production.min.js"></script>

<script type="module">

  const sleep = msec => {
    const start = new Date().getTime();
    while (new Date().getTime() <= start + msec);
  }

  const Test = props => {

    const [state1, setState1] = React.useState(new Date().getTime());
    const [state2, setState2] = React.useState(new Date().getTime());
    const [isPending, startTransition] = React.useTransition();

    return React.createElement(
      React.Fragment, {}, [
        React.createElement("p", {}, "Test New Transition"),
        React.createElement("button", {
          onClick: () => {

            setState1(new Date().getTime());

            startTransition(() => {
              setState2(new Date().getTime());
            });

          }
        }, "Click!"),
        React.createElement("p", {}, state1),
        isPending ?
         React.createElement("p", {}, "Pending...") :
         React.createElement(TestChild1, {caption: state2}),
      ]
    );
  }

  const TestChild1 = props => {
    const c = [];
    for (let i = 0; i < 100; i++) {
      c.push(React.createElement(TestChild2, {caption: `${i}: ${props.caption}`}));
    }
    return React.createElement("p", {}, c);
  }

  const TestChild2 = props => {
    React.useMemo(() => {
      sleep(10); // 重い処理
    }, [props.caption]);
    return React.createElement("p", {}, props.caption);
  }

  const root = ReactDOM.createRoot(document.getElementById("App"));
  root.render(React.createElement(Test));

</script>
```

# おまけ２

 useDeferredValue を使っても同じようなことは実現できます。いってしまえば、startTransition と useDeferredValue の使い分けが現時点ではよく分かりません。
果たして useDeferredValue てホントに必要なものなのですかね？

```test6.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@18.0.0-alpha-e6be2d531/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18.0.0-alpha-e6be2d531/umd/react-dom.production.min.js"></script>

<script type="module">

  const sleep = msec => {
    const start = new Date().getTime();
    while (new Date().getTime() <= start + msec);
  }

  const Test = props => {

    const [state1, setState1] = React.useState(new Date().getTime());
    const [state2, setState2] = React.useState(new Date().getTime());
    const deferredState2 = React.useDeferredValue(state2);

    return React.createElement(
      React.Fragment, {}, [
        React.createElement("p", {}, "Test New Transition"),
        React.createElement("button", {
          onClick: () => {

            setState1(new Date().getTime());

            setState2(new Date().getTime());

          }
        }, "Click!"),
        React.createElement("p", {}, state1),
        React.createElement(TestChild1, {caption: deferredState2}),
      ]
    );
  }

  const TestChild1 = props => {
    const c = [];
    for (let i = 0; i < 100; i++) {
      c.push(React.createElement(TestChild2, {caption: `${i}: ${props.caption}`}));
    }
    return React.createElement("p", {}, c);
  }

  const TestChild2 = props => {
    React.useMemo(() => {
      sleep(10); // 重い処理
    }, [props.caption]);
    return React.createElement("p", {}, props.caption);
  }

  const root = ReactDOM.createRoot(document.getElementById("App"));
  root.render(React.createElement(Test));

</script>
```

# 最後に
いかがでしたでしょうか。React 18 楽しみですね。
少しでも皆様の力になれればうれしいです。