はじめに
use(promise)
の使い方、第2回目です。
今回は「use(promise)
の使い方」というよりも、「Non-Blocking Concurrent Rendering (useTransition
, useDeferredValue
) の使い方」といった方がよいかもしれません。
第1回目で説明済みの点は説明しませんので必ず第1回目も参照してください。
なお、この連載で対象としている fetch 処理は get 系の処理です。post 系の処理は React 19 でいうところの、いわゆる Actions の範疇となります。この区別を 当然の前提 とした記事が多いため混乱しがちですので、この連載を見ている方々にはしっかり両者を区別してしっかり使い分けができるようになっていただきたいと思います。
それではさっそく始めましょう。
use(promise)
with useTransition
use(promise)
を useTransition
と一緒に使う場合は次のようになります。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="initial-scale=1, width=device-width" />
</head>
<body>
<div id="app"></div>
<script type="module">
////////////////////////////////////////////////////////////////////////////////
import React from "https://esm.sh/react@19?dev";
import ReactDOM from "https://esm.sh/react-dom@19?dev";
import ReactDOMClient from "https://esm.sh/react-dom@19/client?dev";
// fetcher
const getResource = async (q) => {
const response = await fetch(`https://dummyjson.com/products/search?q=${q}&delay=1000`);
const data = await response.json();
return data.products;
}
const rss = getResource('');
//
const App = props => {
const [_rss, setrss] = React.useState(rss);
return React.createElement(React.Suspense, {fallback: React.createElement('div', null, 'Now Loading...'), },
React.createElement(MyComponent, {rss:_rss, setrss}),
);
}
//
const MyComponent = props => {
const data = React.use(props.rss);
const [query, setQuery] = React.useState('');
const [isPending, startTransition] = React.useTransition();
const rows = data.map((item, index) => {
return React.createElement('li', {key: item.id}, `${item.title} (${item.price} $)`);
});
const onClick = e => {
const q = String.fromCharCode('a'.charCodeAt(0) + Math.floor(Math.random() * 26));
setQuery(q); // (やってみよう)これも遅延にしたい場合はどうする?
startTransition(() => {
props.setrss(getResource(q));
});
}
return React.createElement('div', null,
React.createElement('button', {onClick}, 'Refetch' + (isPending ? ' 米' :'')),
React.createElement('h1', null, `Products for "${query}"`),
React.createElement('ul', null, rows)
);
}
//
const root = ReactDOMClient.createRoot(document.getElementById("app"));
root.render(React.createElement(App));
////////////////////////////////////////////////////////////////////////////////
</script>
</body>
</html>
onClick
内で startTransition
を利用しています。前回の基本形、応用形が身についている方なら特に難しいと感じるところはないのではないのでしょうか。
ちなみに、startTransition は Suspense がなくても使えます。Suspense の中に容れているのは、初期描画時に fallback を表示させたいがためだけです。Refetch による2度目の描画以降は isPending
が fallback 的な役割を担うことになり、Suspense の fallback が利用されることはありません。 古い記事で「一定の条件下(例えば startTransition 内の処理が一定時間を超えた場合など)で Suspense の fallback が再表示されうる」という旨の記載が見受けられますが、少なくとも React19 ではそのような挙動は確認できませんでした。
なお、startTransition
の引数は action 関数ですが、React 19 からこれを async にできることになりました(「はじめに」で述べた Actions)。もっとも、get 系の処理でそれを async にする意味は皆無です。action 関数を async にして便利になるのは post 系、ということになりますが、それを async にする場合は注意事項が増えるようなのでしっかりドキュメントを読んでから使いましょう。
(やってみよう①)
Products for "〇"
という表示も遅延させたい場合はどうすればよいでしょう?
解答は「最後に」で。
use(promise)
with useDeferredValue
use(promise)
を useDeferredValue
と一緒に使う場合は次のようになります。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="initial-scale=1, width=device-width" />
</head>
<body>
<div id="app"></div>
<script type="module">
////////////////////////////////////////////////////////////////////////////////
import React from "https://esm.sh/react@19?dev";
import ReactDOM from "https://esm.sh/react-dom@19?dev";
import ReactDOMClient from "https://esm.sh/react-dom@19/client?dev";
// fetcher
const getResource = async (q) => {
const response = await fetch(`https://dummyjson.com/products/search?q=${q}&delay=1000`);
const data = await response.json();
return data.products;
}
const rss = getResource('');
//
const App = props => {
const [_rss, setrss] = React.useState(rss);
return React.createElement(React.Suspense, {fallback: React.createElement('div', null, 'Now Loading...'), },
React.createElement(MyComponent, {rss: _rss, setrss}),
);
}
//
const MyComponent = props => {
const data = React.use(React.useDeferredValue(props.rss));
const [query, setQuery] = React.useState('');
const rows = data.map((item, index) => {
return React.createElement('li', {key: item.id}, `${item.title} (${item.price} $)`);
});
const onClick = e => {
const q = String.fromCharCode('a'.charCodeAt(0) + Math.floor(Math.random() * 26));
setQuery(q); // (やってみよう)これも遅延にしたい場合はどうする?
props.setrss(getResource(q));
}
return React.createElement('div', null,
React.createElement('button', {onClick}, 'Refetch'),
React.createElement('h1', null, `Products for "${query}"`),
React.createElement('ul', null, rows)
);
}
//
const root = ReactDOMClient.createRoot(document.getElementById("app"));
root.render(React.createElement(App));
////////////////////////////////////////////////////////////////////////////////
</script>
</body>
</html>
useDeferredValue
は引数と戻り値が同じ型になるので設置場所の自由度が高く使いやすいですが isPending
がないので fallback 的な処理ができません。
(やってみよう②)
Products for "〇"
という表示も遅延させたい場合はどうすればよいでしょう?
解答は「最後に」で。
useSyncExternalStore
と Non-Blocking Concurrent Rendering
前回、useSyncExternalStore
について、 useTransition
との相性はよくないが不思議なことに useDeferredValue
とはそんなことはない旨を記載しました。本当かどうか確認してみましょう。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="initial-scale=1, width=device-width" />
</head>
<body>
<div id="app"></div>
<script type="module">
////////////////////////////////////////////////////////////////////////////////
import React from "https://esm.sh/react@19?dev";
import ReactDOM from "https://esm.sh/react-dom@19?dev";
import ReactDOMClient from "https://esm.sh/react-dom@19/client?dev";
// fetcher
const getResource = async (q) => {
const response = await fetch(`https://dummyjson.com/products/search?q=${q}&delay=1000`);
const data = await response.json();
return data.products;
}
let rss = getResource('');
// Store の機能
const subscribe = notify => {
// 購読の申し込み
setSnapshot = newrss => {
rss = newrss; // snapshot の更新
notify(); // 購読者に通知
};
return () => {}; // // 購読の解除: 今回は何もする必要がない
};
const getSnapshot = () => rss; // snapshot の取得
let setSnapshot; // snapshot の更新
//
const App = props => {
return React.createElement(React.Suspense, {fallback: React.createElement('div', null, 'Now Loading...'), },
React.createElement(MyComponent),
);
}
//
const MyComponent = props => {
const _rss = React.useSyncExternalStore(subscribe, getSnapshot);
const data = React.use(_rss);
const [query, setQuery] = React.useState('');
const [isPending, startTransition] = React.useTransition();
const rows = data.map((item, index) => {
return React.createElement('li', {key: item.id}, `${item.title} (${item.price} $)`);
});
const onClick = e => {
const q = String.fromCharCode('a'.charCodeAt(0) + Math.floor(Math.random() * 26));
setQuery(q);
startTransition(() => {
setSnapshot(getResource(q));
});
}
return React.createElement('div', null,
React.createElement('button', {onClick}, 'Refetch' + (isPending ? ' 米' :'')),
React.createElement('h1', null, `Products for "${query}"`),
React.createElement('ul', null, rows)
);
}
//
const root = ReactDOMClient.createRoot(document.getElementById("app"));
root.render(React.createElement(App));
////////////////////////////////////////////////////////////////////////////////
</script>
</body>
</html>
Refecth で Suspense の fallback が表示されてしまうこと、すなわち startTransition
の恩恵がまったく得られないことが確認できるはずです。
次に、useDeferredValue
の場合を見てみましょう。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="initial-scale=1, width=device-width" />
</head>
<body>
<div id="app"></div>
<script type="module">
////////////////////////////////////////////////////////////////////////////////
import React from "https://esm.sh/react@19?dev";
import ReactDOM from "https://esm.sh/react-dom@19?dev";
import ReactDOMClient from "https://esm.sh/react-dom@19/client?dev";
// fetcher
const getResource = async (q) => {
const response = await fetch(`https://dummyjson.com/products/search?q=${q}&delay=1000`);
const data = await response.json();
return data.products;
}
let rss = getResource('');
// Store の機能
const subscribe = notify => {
// 購読の申し込み
setSnapshot = newrss => {
rss = newrss; // snapshot の更新
notify(); // 購読者に通知
};
return () => {}; // // 購読の解除: 今回は何もする必要がない
};
const getSnapshot = () => rss; // snapshot の取得
let setSnapshot; // snapshot の更新
//
const App = props => {
return React.createElement(React.Suspense, {fallback: React.createElement('div', null, 'Now Loading...'), },
React.createElement(MyComponent),
);
}
//
const MyComponent = props => {
const _rss = React.useSyncExternalStore(subscribe, getSnapshot);
const data = React.use(React.useDeferredValue(_rss));
const [query, setQuery] = React.useState('');
const rows = data.map((item, index) => {
return React.createElement('li', {key: item.id}, `${item.title} (${item.price} $)`);
});
const onClick = e => {
const q = String.fromCharCode('a'.charCodeAt(0) + Math.floor(Math.random() * 26));
setQuery(q);
setSnapshot(getResource(q));
}
return React.createElement('div', null,
React.createElement('button', {onClick}, 'Refetch'),
React.createElement('h1', null, `Products for "${query}"`),
React.createElement('ul', null, rows)
);
}
//
const root = ReactDOMClient.createRoot(document.getElementById("app"));
root.render(React.createElement(App));
////////////////////////////////////////////////////////////////////////////////
</script>
</body>
</html>
きちんと Non-Blocking Concurrent Rendering されるはずです。不平等(?)ですね。
(やってみよう③)
Jotai の atom を使った場合における Non-Blocking Concurrent Rendering を試してみよう。
前回お話しした通り Jotai の atom は、useTransition
にも useDeferredValue
にも見事に対応いただいていることが確認できるはずです。
ここまでついてきていただいた皆さんであれば解答は不要であると思いますので解答はあげません。
(おまけ)useOptimistic
の使用例
まったく今回の話とは無関係な post 系の話ですが、これを単独の記事とするほどのものではないのでここに書いておきます。興味のない方は読み飛ばしてください。
「はじめに」で少し述べた React19 で追加された Actions については 公式ブログのこの記事 で定義されてます(日本語訳の「アクション」ですと少し紛らわしいことがあるのであえて Actions と書いています)。
それによれば、useAsctionState
Hook などの Hooks そのものが Actions なのではなく、useAsctionState
Hook の引数である async 関数や、startTransition
の引数のことを Actions というらしいです。
さて、React19 で追加された useOptimistic
Hook は大変便利な Hook ですので利用する場面が増えると思うのですが、他の Hook とはやや使い勝手が異なりますのでやや注意が必要です。
なお、startTransition
はなくても動作します。post 処理が比較的短いことが断定できる場合などは UI 的な工夫が不要なわけですから無理に startTransition
等を使用する必要はありません。
const [state, setState] = useState(1); // startTransition の Actions で利用する場合は必須
const [value, setter] = useOptimistic(state/*, updateFn*/); // 第1引数は useState と違いいわゆる initValue ではないので注意。第2引数の更新用関数は省略可。
const handleClick = () => {
startTransition(async() => { // Actions
setter(2); // 2 になることを期待してます!
// ★★★ ここで await fetch(..., {method: 'POST'}) などを実行する。
setState(2); // 成功したので2 になりました!(仮に POST などが失敗した場合は例外が発生しこれが実行されないため 1 のまま。なお、React 19 においては Actions 内における await の後ろの setter は Transition としてマークされないらしいが、このような一般的な例では実質問題とならない)
});
}
return (<button onClick={handleClick}>Value is {value}</button>);
動作フロー:
-
Value is 1
と表示される。 - ボタンをクリックすることで Actions の実行が開始される。
- Actions 内で
setter
が呼び出される。 -
setter
が新しい値2
で(updateFn が指定されていたらそれを通した上で)value
を更新しValue is 2
と再描画される。 - Actions 内で post 系の処理をする(今回の例では省略)。
- post 系処理が成功した場合は、
setState(2)
が実行されてstate
に2
が設定され、Value is 2
と再描画される。 - post 系処理が失敗した場合は、例外処理や if 文などを使い
setState(2)
の実行を回避することによりstate
が1
のままとなるため、Actions 完了後Value is 1
と再描画される。
以上の点さえ押さえれば Master of useOptimistic となれるはずです。
なお、React 19.1 における useOptimistic
Hook の TypeScript 用の型情報は次の通りで、これを見る限り、第2引数は省略できるようです。
export function useOptimistic<T>(
passthrough: T,
reducer?: (state: T, optimisticValue: unknown) => T
): [T, (optimisticValue: T) => void];
(おまけ)Concurrent と Parallel
Concurrent(並行)と Parallel(並列)は異なる概念で、Golang 界隈で軽い気持ちで使用すると「厳密に区別せよ」としばしば怒られたりしますが、実際には両者の違いはそんな難しいものではなく、とりあえず Concurrent(並行)を使っておけば怒られない という程度のものです。
なぜならば、Parallel(並列)とは、各タスクが協調動作(例えば変数の共有利用など)をせずに独立して作動するもののことをいうからです。逆にいえば、何らかの協調動作をした時点でそれは Concurrent(並行)というものになってしまうわけです。
独立動作しているものを Concurrent(並行)と呼んでしまったとしても、所詮 OS のスレッドレベルの掌の中の話である限りそれを間違いと立証するのはなかなか困難です(端末が2台並んでいて本当に両端末が全く無関係に作動しているだけなら「どこに並行がありえんねん!」と怒られるでしょうが)。
最後に
いかがでしたでしょうか。少しでも皆様の参考になれば幸いです。
この拙文をすべて読み切ったあなたには Master of use(promise) の称号を与えます。
いや、正直、この連載で書いていないことは知る必要もないことといっても過言ではないので(今のうちに謝罪します。私の思い込みです)、 Async Master of React19 の称号を与えてもよいくらいのキモチです!
やってみよう①の解答
startTransition(() => {
setQuery(q); // ここに容れる
props.setrss(getResource(q));
});
簡単ですね。他にも、
startTransition(() => {
setQuery(q); // ここに容れる
});
startTransition(() => {
props.setrss(getResource(q));
});
や、useTransition
を2つ使って
startTransition1(() => {
setQuery(q); // ここに容れる
});
startTransition2(() => {
props.setrss(getResource(q));
});
も React19 では正解ですが、React のバージョンアップにより動作が変わる可能性はあります。
やってみよう②の解答
const [_query, setQuery] = React.useState('');
const query = React.useDeferredValue(_query);
こちらは色々な書き方があると思いますのできちんと動作さえしていればすべて正解でよいと思います。
なお、startTransition
と deferredValue
を組み合わせることもできます。すなわち、同じコンポーネント内の startTransition
と deferredValue
は、協調(もっとも遅い描画の準備が整うまで待つ)して動作するみたいです。実験してみてください。