はじめに
Suspense の後、useTransition が登場します
一.Suspense だけでは不十分か?
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
Suspense サブツリー内にまだ戻っていない Lazy コンポーネントが存在すれば、fallback で指定されたコンテンツに進み、任意の祖先レベルの loading に引き上げることができます。
Suspense コンポーネントは(コンポーネントツリー内の)Lazy コンポーネントの上方の任意の位置に配置でき、かつ下方に複数の Lazy コンポーネントを持つことができます。
loading シーンだけから見ると、Suspense は 2 つの能力を提供します:
-
loading の引き上げをサポート
-
loading の集約をサポート
ユーザー体験而言、2 方面のメリットがあります:
-
レイアウトジッターの回避(データが戻ってきた後に一塊のコンテンツが現れる)
-
異なるネットワーク環境への対応(データ戻りが速ければ loading は現れない)
前者は loading(または skeleton)によるメリットで、後者は Concurrent Mode 下の間欠スケジューリングによるものです
P.S.Suspense の詳細情報については、React Suspense——コード分割に loading を追加することから…… を参照
Suspense が提供する優雅で柔軟、人間性の高い loading はすでに極致の開発体験とユーザー体験に達しているように見えますが、さらに探索すると、loading を巡っていくつかの問題があります:
-
loading を追加すれば、体験は必ず良くなるのか?
-
すぐに loading を表示することの何が問題か?
-
インタラクションのリアルタイム応答と loading の衝突をどう解決するか?
-
削減できない長い loading に対して、ユーザー感知上もっと速くする方法はあるか?
-
レイアウトジッターは本当に存在しなくなったのか?リスト内に複数の loading が同時に存在したら?
次に、これらの問題を一つずつ検討します
二.視覚的に loading を弱化
loading を追加すれば、体験は必ず良くなるのか?
典型的なページネーションリストを例にすると、一般的なインタラクションプロセスは以下のようになります:
1.1 ページ目のコンテンツが表示
2. 次のページをクリック
3. 1 ページ目のコンテンツが消失、または半透明のレイヤーで覆われる
4. loading が表示
5. しばらくして loading が消失
6. 2 ページ目のコンテンツが表示
その中で最大���問題は、loading 期間中 1 ページ目のコンテンツが使用不可(不可視、または覆われている)ことです。つまり、loading がページコンテンツの完全性と、アプリケーションの応答能力(responsiveness)に影響しています
既然如此、いっそ loading を削除しましょう:
1.1 ページ目のコンテンツが表示
2. 次のページをクリック
3. 1 ページ目のコンテンツは元のまま
...インタラクションフィードバックなし、数秒後
4. 2 ページ目のコンテンツが表示
即時インタラクションフィードバックが不足しているため、ユーザー体験はさらに悪化しました。では、両立する方法 はないでしょうか。loading 期間中の応答性を保証しつつ、loading に似たインタラクション体験も得られる方法です
あります。loading の視覚効果を弱化します:
-
グローバル loading(またはコンテンツブロック loading)をローカル loading に弱化:loading がコンテンツ完全性を破壊するのを回避
-
グレーアウトなどの方法で表示中が旧コンテンツであることを暗示:旧コンテンツによるユーザーの混乱を回避
例えば、ボタンクリックのシーンでは、loading フィードバックをボタン上に簡単に追加できます:
//...
render() {
const { isLoading } = this.state;
return (
<Page>
<Content style={{ color: isLoading ? "black" : "gray" }} />
<Button>{isLoading ? "Next" : "Loading..."}</Button>
</Page>
);
}
loading プロセス中ユーザーが見ている仍然是完全なコンテンツ(一部コンテンツは少し古いが、グレーアウトで暗示されている)ことを保証するだけでなく、即時にインタラクションフィードバックも提供できます
大多数の場合、上記の例のようにすぐに loading を表示しても問題ありませんが、他のシーンでは、迅速に現れる loading は期待通りではありません
三.論理的に loading を遅延
すぐに loading を表示することの何が問題か?
loading が非常に速い場合(100ms のみ)、ユーザーは何かが一瞬光っただけと感じるかもしれません……また別の悪いユーザー体験です
もちろん、このようなシーンでは通常 loading を追加しません。loading は通常ユーザーに「遅い」という心理的期待をもたらし、元々非常に速い操作に loading を追加すると、ユーザー感知上の速度体験を低下させることになるため、追加しないことを選びます
しかし、非常に速い場合もあれば、非常に遅い場合もある操作がある場合、loading は追加すべきか追加すべきでないか?
この時按需 loading が必要です。例えば loading タイミングを遅延し、200ms 後新コンテンツがまだ準備できていない場合のみ loading を表示
React はこのシーンを考慮し、useTransition が生まれました
useTransition
Transition 特性は Hooks API 形式で提供されます:
const [startTransition, isPending] = React.useTransition({
timeoutMs: 3000
});
P.S.注意、Transition 特性は Concurrent Mode に依存し、かつ現在(2019/11/23)は正式にリリースされていません(実験的特性)、具体的な API はさらに変化する可能性があります、参考のみ、試遊は Transitions を参照
Transition Hook の役割は、State の更新を遅延しても問題ないと React に伝えることです:
Wrap state update into startTransition to tell React it's okay to delay it.
例えば:
function App() {
const [resource, setResource] = useState(initialResource);
const [startTransition, isPending] = React.useTransition({
timeoutMs: 3000
});
return (<>
<button
disabled={isPending}
onClick={() => {
startTransition(() => {
const nextUserId = getNextId(resource.userId);
setResource(fetchProfileData(nextUserId));
});
}}
>
Next
</button>
{isPending ? " Loading..." : null}
<ProfilePage resource={resource} />
</>);
}
function ProfilePage({ resource }) {
return (<Suspense fallback={<h1>Loading profile...</h1>} >
<ProfileDetails resource={resource} />
<Suspense fallback={<h1>Loading posts...</h1>} >
<ProfileTimeline resource={resource} />
</Suspense>
</Suspense>);
}
-
Nextボタンをクリックして直ちにProfileDataを取得、その後isPendingがtrueになり、Loading...が表示 -
ProfileDataが 3 秒以内に戻れば、(表示中の旧ProfilePageから切り替えて)新ProfilePageコンテンツを表示 -
そうでなければ
ProfilePageの Suspense fallback に入り、(旧ProfilePageが消失)Loading profile...が表示
つまり、startTransition は本来直ちに ProfilePage に渡されるべき(まだ取得できていない)resource 状態値を遅延させ、最大 3 秒遅延します。这正是私たちが求める按需 loading 能力です:timeoutMs ミリ秒内は loading せず、タイムアウト后才 loading を表示
したがって、簡単に言えば、Transition は Suspense を delay できます。つまり、Transition は loading を遅延できます
按需 loading
ページコンテンツ状態から見ると、Transition は旧コンテンツ仍然使用可能な Pending 状態を導入しました:

各状態の意味は以下の通り:
-
Receded(消失):現在ページコンテンツが消失、Suspense fallback に降格
-
Skeleton(スケルトン):新ページが既に現れ、一部新コンテンツ仍然加载中
-
Pending(待機中):新コンテンツが進行中、現在ページコンテンツは完全、仍然インタラクション可能
Pending を提案した出発点は後退(既に存在するコンテンツを隠す)を回避することです:
However, the Receded state is not very pleasant because it "hides" existing information. This is why React lets us opt into a different sequence (Pending → Skeleton → Complete) with useTransition.
簡単に loading を追加した場合の対応する状態変化は Receded → Skeleton → Complete(速いか遅いかに関わらず、loading を表示)で、Transition 有了後、体験が最適な状況は Pending → Skeleton → Complete(非常に速く、loading は不要)、少し悪いのは Pending → Receded → Skeleton → Complete(非常に遅く、loading しないわけにはいかない)
したがって、最適な体験のために、Pending 時間を短縮し、できるだけ早く Skeleton 状態に入るべきです。小さなテクニックは遅い、および重要でないコンポーネントを Suspense で包むことです:
Instead of making the transition shorter, we can "disconnect" the slow component from the transition by wrapping it into
ベストプラクティス
同時に、Hooks の細粒度論理再利用方面のメリット により、簡単に Transition の按需 loading 効果を基礎コンポーネントにカプセル化できます。例えば Button:
function Button({ children, onClick }) {
const [startTransition, isPending] = useTransition({
timeoutMs: 10000
});
function handleClick() {
startTransition(() => {
onClick();
});
}
const spinner = (
// ...
);
return (<>
<button onClick={handleClick} disabled={isPending}>
{children}
</button>
{isPending ? spinner : null}
</>);
}
这也是公式推奨の做法で、UI コンポーネントライブラリが useTransition が必要なシーンを考慮する ことで、冗長コードを削減します:
Pretty much any button click or interaction that can lead to a component suspending needs to be wrapped in useTransition to avoid accidentally hiding something the user is interacting with.
This can lead to a lot of repetitive code across components. This is why we generally recommend to bake useTransition into the design system components of your app.
四.インタラクションのリアルタイム応答と loading の衝突を解決
インタラクションのリアルタイム応答と loading の衝突をどう解決するか?
Transition が loading 表示を遅延できるのは、State 更新を遅延したためです。では遅延できない State 更新の場合はどうでしょうか。例えば入力値:
function App() {
const [query, setQuery] = useState(initialQuery);
function handleChange(e) {
const value = e.target.value;
setQuery(value);
}
return (<>
<input value={query} onChange={handleChange} />
<Suspense fallback={<p>Loading...</p>}>
<Translation input={query} />
</Suspense>
</>);
}
ここで input を [制御コンポーネント](/articles/从 componentwillreceiveprops 说起/#articleHeader5) として使用しています(onChange でユーザー入力を処理)。そのため新 value を直ちに State に更新する必要があります。そうでないと入力遅延、さらには混乱が発生します
したがって、衝突が発生しました。このリアルタイム応答入力の要求と Transition の State 更新遅延は共存できないように見えます
公式が提供する解決策はこの状態値を冗長化することです。衝突があるなら、いっそ分けてそれぞれ使用します:
function App() {
const [query, setQuery] = useState(initialQuery);
const [resource, setResource] = useState(initialResource);
const [startTransition, isPending] = useTransition({
timeoutMs: 5000
});
function handleChange(e) {
const value = e.target.value;
// Outside the transition (urgent)
setQuery(value);
startTransition(() => {
// Inside the transition (may be delayed)
setResource(fetchTranslation(value));
});
}
return (<>
<input value={query} onChange={handleChange} />
<Suspense fallback={<p>Loading...</p>}>
<Translation resource={resource} />
</Suspense>
</>);
}
React の実践経験は計算できるなら計算し、共有可能なら共有し、状態値を冗長化しないよう教えてくれます。メリットは状態更新時の漏れを回避できることです:
This lets us avoid mistakes where we update one state but forget the other state.
而我们刚刚也确实冗長化了一个状態値(query と resource)、実践原則を覆すわけではなく、State に優先順位を区別できるということです:
-
高優先 State:更新を delay したくない State。例えば入力値
-
低優先 State:delay が必要な状態。例えば Transition 関連
つまり、Transition 有了後、State に優先順位ができました
五.UI 一貫性の犠牲を考慮
削減できない長い loading に対して、ユーザー感知上もっと速くする方法はあるか?
あります。UI 一貫性を犠牲にする意思があれば
聞き間違いではありません。UI 一貫性も揺るぎないものではなく、必要時にはUI 一貫性を犠牲にして感知上より良い体験効果を得ることを考慮できます。「題と合わない」状況が発生しますが、10 秒またはそれ以上 loading を表示するより友好的な場合もあります。同様に、グレーアウト暗示などの手段でユーザーに UI 不一致の事実を認識させることができます
为此、React は DeferredValue Hook を提供します
useDeferredValue
const deferredResource = React.useDeferredValue(resource, {
timeoutMs: 1000
});
// 用法
<ProfileTimeline
resource={deferredResource}
isStale={deferredResource !== resource} />
P.S.注意、現在(2019/11/23)は useDeferredValue が正式にリリースされていません、具体的な API はさらに変化する可能性があります、参考のみ、試遊は Deferring a Value を参照
Transition 機制と類似し、状態更新を遅延することに相当し、新データが準備できる前に、旧データを引き続き使用できます。1 秒以内に新データが来れば、(旧コンテンツから切り替えて)新コンテンツを表示し、そうでなければ直ちに状態を更新し、loading すべきなら loading します
Transition との違いは、useDeferredValue は状態値面向で、Transition は状態更新操作面向で、API および意味上の差異であり、機制上二者は非常に似ています
六.レイアウトジッターを彻底して消除
レイアウトジッターは本当に存在しなくなったのか?リスト内に複数の loading が同時に存在したら?
複数 loading 併存のシーンでは、loading の先後順序が異なることによるレイアウトジッターが発生しがちです。視覚効果上、通常元の一塊のものが一方に押しやられることを望みません(視覚的には append であり、insert であってはなりません)。レイアウトジッターを彻底して消除するには、2 つの思路があります:
-
すべてのリスト項目を同時表示:すべての項目が準備できるまで待ってから表示するが、待機時間が上がる
-
リスト項目をその相対順序に従って出現させる:insert を消除でき、待機時間も常に最悪ではない
では、非同期コンテンツの出現(loading 消失)順序をどのように制御するか?
React も考慮しており、SuspenseList を提供して Suspense コンテンツのレンダリング順序を制御し、リスト内要素の表示順序が相対位置に従うことを保証し、コンテンツが押しやられるのを回避します:
<SuspenseList> coordinates the "reveal order" of the closest <Suspense> nodes below it
SuspenseList
import { SuspenseList } from 'react';
function ProfilePage({ resource }) {
return (
<SuspenseList revealOrder="forwards">
<ProfileDetails resource={resource} />
<Suspense fallback={<h2>Loading posts...</h2>}>
<ProfileTimeline resource={resource} />
</Suspense>
<Suspense fallback={<h2>Loading fun facts...</h2>}>
<ProfileTrivia resource={resource} />
</Suspense>
</SuspenseList>
);
}
revealOrder="forwards" は SuspenseList 下の子級 Suspense が上から下への順序で出現する必要があることを示し、誰のデータが先に準備できても同様です。類似の値に backwards(逆順出現)と together(同時出現)があります
また、複数の loading が同時に現れることによるユーザーへの体験混乱を回避するために、tail オプションも提供しています。詳細は SuspenseList を参照
P.S.注意、現在(2019/11/23)は SuspenseList が正式にリリースされていません、具体的な API はさらに変化する可能性があります、参考のみ、試遊は SuspenseList を参照
七.まとめ
私たちが目撃したように、極致体験を追求する大道上で、React はますます遠くまで進んでいます:
-
Suspense:優雅で柔軟、人間性の高いコンテンツ降格をサポート
-
useTransition:按需降格をサポートし、本当に遅い場合のみ降格
-
useDeferredValue:UI 一貫性を犠牲にして感知上より良い体験効果を得ることをサポート
-
SuspenseList:一組の降格効果の出現順序、および併存数量を制御することをサポート
P.S.最も簡単な降格戦略は loading で、其它はキャッシュ値を使用、さらには広告を一段落、ミニゲームを開始なども降格とみなします
コメントはまだありません