1
1

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 3 years have passed since last update.

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

Last updated at Posted at 2021-06-18

この記事について

次期 React 18 の概要が発表されました。この記事では、React 18 の新機能である startTransition をについて理解を深めたいと思います。

まずは 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 で囲ってみました・・・が、ボタンはやはり1秒間押せないままです。ただ、state1 と state2 を描画するタイミングが、test1.html と test2.html では異なるようです。どういうことなのでしょうか。

描画タイミングを調べる

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

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() で1回、setState2() で1回と、計2回レンダリングされているみたいですね。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 は、理解せずただやみくもに使っても意味がない。
  2. 重い処理は、ユーザーの入力を阻害するほど重いものであってはならない。すなわち、適度にスライスされた処理でなければならない。同期処理ではうまくスライスできないくらいに重すぎる処理は Promise 化、すなわち Suspense を用いてスライスしてあげる必要がある。
  3. 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>

おまけ2

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 楽しみですね。
少しでも皆様の力になれればうれしいです。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?