この記事について
次期 React 18 の概要が発表されました。この記事では、React 18 の新機能である startTransition をについて理解を深めたいと思います。
まずは startTransition を使わない場合
いきなりですが、startTransition を使わない場合のソースです
<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 を導入します!
<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回起こっているようですので確認してみます。
<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 などを用いて重い処理を避けるようにすればよさそうです。
完成
完成したソースです。ボタンが繰り返しクリックできるようになりました!
<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>
分かったこと
- startTransition は、理解せずただやみくもに使っても意味がない。
- 重い処理は、ユーザーの入力を阻害するほど重いものであってはならない。すなわち、適度にスライスされた処理でなければならない。同期処理ではうまくスライスできないくらいに重すぎる処理は Promise 化、すなわち Suspense を用いてスライスしてあげる必要がある。
- useMemo などのメモ化機能がいよいよその本領を発揮してきた。
おまけ
useTransition も同じように試してみました。素敵ですね。
<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 てホントに必要なものなのですかね?
<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 楽しみですね。
少しでも皆様の力になれればうれしいです。