Closed45

Concurrent Mode関連機能のDocsをまとめて読む

hajimismhajimism
hajimismhajimism

個人的には、他の機能と比べると圧倒的に使う頻度が高い

hajimismhajimism

<Suspense> lets you display a fallback until its children have finished loading.

hajimismhajimism

SSRの発明によって一定の最適化ができたとした上で、下記の3つの問題点が残っていた。

  1. fetch everything before you can show anything
  2. load everything before you can hydrate anything
  3. hydrate everything before interact with anything

Suspenseがこの3つを解決してくれるという動画だった。

hajimismhajimism

Reactが表示に関わる処理の順番についてすべての責任を負うので、開発者は「何を表示するか」だけを考えていれば良く、宣言的UIが保たれている

hajimismhajimism

ドキュメントに戻る

If Suspense was displaying content for the tree, but then it suspended again, the fallback will be shown again unless the update causing it was caused by startTransition or useDeferredValue.

他のAPIとの関係性が示されているのでメモ

hajimismhajimism

Only Suspense-enabled data sources will activate the Suspense component. They include:

  • Data fetching with Suspense-enabled frameworks like Relay and Next.js
  • Lazy-loading component code with lazy

Suspenseネイティブなdata fetching libraryはまだまだ限られているのかな

hajimismhajimism

Don’t put a Suspense boundary around every component. Suspense boundaries should not be more granular than the loading sequence that you want the user to experience. If you work with a designer, ask them where the loading states should be placed—it’s likely that they’ve already included them in their design wireframes.

Suspense境界をほいほい敷くな。意味のあるまとまりごとにやれ。

hajimismhajimism

Suspenseそのものの機能の説明についてはあまり特筆するところがないな、使い慣れているからかな。App Routerのドキュメントにも同じような例が書いてあるんだよな。

hajimismhajimism

useDeferredValueの例でてきた。Suspenseの内部でdeferred valueの更新があってもfallbackが再表示されることはなく、deferredな更新が行われるのみ。一度表示したものをちらつかせずに更新できるのが良い。

A common alternative UI pattern is to defer updating the list and to keep showing the previous results until the new results are ready. The useDeferredValue Hook lets you pass a deferred version of the query down:

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={deferredQuery} />
      </Suspense>
    </>
  );
}
hajimismhajimism

deferred valueだと最新のstateと比較することでisPendingみたいな値が取れるのか。もしかして内部的にもこんな感じなのかな?

const isStale = query !== deferredQuery;
hajimismhajimism

Both deferred values and transitions let you avoid showing Suspense fallback in favor of inline indicators.

"avoid showing Suspense fallback"という感覚は大事だろうな。

hajimismhajimism

setState FunctionにアクセスできるときはstartTransitionを使う

  function navigate(url) {
    startTransition(() => {
      setPage(url);      
    });
  }
hajimismhajimism

Suspense-enabled routers are expected to wrap the navigation updates into transitions by default.

これ知らないとNext.jsの挙動をちゃんと説明できないな

hajimismhajimism

On the client, React will attempt to render the same component again. If it errors on the client too, React will throw the error and display the closest error boundary. However, if it does not error on the client, React will not display the error to the user since the content was eventually displayed successfully.

これやってみたけれども、開発中はやはりErrorが開発者に見える形で表示されるので、Errorを投げるのはイヤな感じがした。

<Suspense fallback={<Loading />}>
  <Chat />
</Suspense>

function Chat() {
  if (typeof window === 'undefined') {
    throw Error('Chat should only render on the client.');
  }
  // ...
}
hajimismhajimism

今回見たYouTubeもそうだけど、React周り良い資料多すぎる。

hajimismhajimism
hajimismhajimism

useTransition is a React Hook that lets you update the state without blocking the UI.

  const [isPending, startTransition] = useTransition();

ちなみにstartTransitionの解説は「useTransitionのやつと一緒だよー」てことでかなり省略されている。
https://react.dev/reference/react/startTransition

hajimismhajimism

scope: A function that updates some state by calling one or more set functions. React immediately calls scope with no parameters and marks all state updates scheduled synchronously during the scope function call as transitions. They will be non-blocking and will not display unwanted loading indicators.

このnon-blockingという言葉の感覚がイマイチ掴めてない。

hajimismhajimism

You can wrap an update into a transition only if you have access to the set function of that state. If you want to start a transition in response to some prop or a custom Hook value, try useDeferredValue instead.

set functionにアクセスできる場合はstartTransition、値にしかアクセスできないときはuseDeferredValue。

hajimismhajimism

The function you pass to startTransition must be synchronous. React immediately executes this function, marking all state updates that happen while it executes as transitions. If you try to perform more state updates later (for example, in a timeout), they won’t be marked as transitions.

おおーここ重要だ。わかる気もするけど、でもなんでだろう。

hajimismhajimism

Transition updates can’t be used to control text inputs.

Caveatsの中ではいやに具体的で目立つな。

hajimismhajimism

With a transition, your UI stays responsive in the middle of a re-render. For example, if the user clicks a tab but then change their mind and click another tab, they can do that without waiting for the first re-render to finish.

あ、non-blockingてこういうことか。即座に他の処理に移ることができるということ。

hajimismhajimism

親の状態変化を子でtransitionとしてマークすることもできる。isPendingを取りたい場所を柔軟に選ぶことができるのがメリット?

import { useTransition } from 'react';

export default function TabButton({ children, isActive, onClick }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        onClick();
      });
    }}>
      {children}
    </button>
  );
}

hajimismhajimism

Transitions will only “wait” long enough to avoid hiding already revealed content (like the tab container). If the Posts tab had a nested <Suspense> boundary, the transition would not “wait” for it.

TransitionとSuspenseの関係性を端的に表している文

hajimismhajimism

You can’t use a transition for a state variable that controls an input:

const [text, setText] = useState('');
// ...
function handleChange(e) {
  // ❌ Can't use transitions for controlled input state
  startTransition(() => {
    setText(e.target.value);
  });
}
// ...
return <input value={text} onChange={handleChange} />;

This is because transitions are non-blocking, but updating an input in response to the change event should happen synchronously.

うーん、わかるようでわからん

hajimismhajimism

Alternatively, you can have one state variable, and add useDeferredValue which will “lag behind” the real value. It will trigger non-blocking re-renders to “catch up” with the new value automatically.

これはなんとなく自然に理解できる。

hajimismhajimism

これもわかるようでわからんな...

startTransition(() => {
  // ❌ Setting state *after* startTransition call
  setTimeout(() => {
    setPage('/about');
  }, 1000);
});

startTransition(async () => {
  await someAsyncFunction();
  // ❌ Setting state *after* startTransition call
  setPage('/about');
});
hajimismhajimism

React executes your function immediately, but any state updates scheduled while it is running are marked as transitions.

Commitが遅延されているだけで、Render自体は即座に(そして並行していくつも)やっているっていうことでいいのかな?

hajimismhajimism

うわー、この説明Dan先生っぽい!

// A simplified version of how React works

let isInsideTransition = false;

function startTransition(scope) {
  isInsideTransition = true;
  scope();
  isInsideTransition = false;
}

function setState() {
  if (isInsideTransition) {
    // ... schedule a transition state update ...
  } else {
    // ... schedule an urgent state update ...
  }
}

内部実装読んでみなきゃだなー

hajimismhajimism
hajimismhajimism

useDeferredValue is a React Hook that lets you defer updating a part of the UI.

hajimismhajimism

The values you pass to useDeferredValue should either be primitive values (like strings and numbers) or objects created outside of rendering. If you create a new object during rendering and immediately pass it to useDeferredValue, it will be different on every render, causing unnecessary background re-renders.

大事すぎるポイント

hajimismhajimism

When useDeferredValue receives a different value (compared with Object.is), in addition to the current render (when it still uses the previous value), it schedules a re-render in the background with the new value. The background re-render is interruptible: if there’s another update to the value, React will restart the background re-render from scratch. For example, if the user is typing into an input faster than a chart receiving its deferred value can re-render, the chart will only re-render after the user stops typing.

再レンダリングをスケジュールしておいて畳み込むイメージかな

hajimismhajimism

useDeferredValue does not by itself prevent extra network requests.

おーこれも大事。あくまでCommitをdeferしているだけでrenderingは更新の数だけやってるもんね。

hajimismhajimism

The background re-render caused by useDeferredValue does not fire Effects until it’s committed to the screen. If the background re-render suspends, its Effects will run after the data loads and the UI updates.

さすがReactって感じ。

hajimismhajimism

Let’s walk through an example to see when this is useful.

あれ、これまでこういう言葉が太字にされることはなかったのに!w

hajimismhajimism

How does deferring a value work under the hood?

You can think of it as happening in two steps:

  • First, React re-renders with the new query ("ab") but with the old deferredQuery (still "a"). The deferredQuery value, which you pass to the result list, is deferred: it “lags behind” the query value.
  • In background, React tries to re-render with both query and deferredQuery updated to "ab". If this re-render completes, React will show it on the screen. However, if it suspends (the results for "ab" have not loaded yet), React will abandon this rendering attempt, and retry this re-render again after the data has loaded. The user will keep seeing the stale deferred value until the data is ready.

render-as-you-fetchそのものという感じがするね

hajimismhajimism

Note that there is still a network request per each keystroke. What’s being deferred here is displaying results (until they’re ready), not the network requests themselves. Even if the user continues typing, responses for each keystroke get cached, so pressing Backspace is instant and doesn’t fetch again.

あーそうか、だからcacheの話が大事になってくるのか、なるほど、たしかに

hajimismhajimism

useDeferredValueとコンポーネントのmemo化を組み合わせることで、パフォーマンスの最適化を図ることができる。

const SlowList = memo(function SlowList({ text }) {
  // ...
});
function App() {
  const [text, setText] = useState('');
  const deferredText = useDeferredValue(text);
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <SlowList text={deferredText} />
    </>
  );
}

textが更新されてre-renderされてもpropであるdeferredTextが更新されない限りSlowListは再レンダリングしなくて済む。

逆に言えば、memo化を忘れてたらdeferする意味がないよ。

Without memo, it would have to re-render anyway, defeating the point of the optimization.

hajimismhajimism

How is deferring a value different from debouncing and throttling?

関連テクにdebouncing や throttlingがある。

  • Debouncing means you’d wait for the user to stop typing (e.g. for a second) before updating the list.
  • Throttling means you’d update the list every once in a while (e.g. at most once a second).

これらと比較して、useDeferredValueには2つのメリットがある。1つは固定遅延が無くてスリムであること。もう1つはnon-blockingであること。これらの利点からより良いUXを提供できる。

ただdebouncing や throttlingはnetwork requestsを減らすためには有用なテクなので、使い分けが大切であるとのこと。

このスクラップは2023/08/10にクローズされました