Open4

React

expsh13expsh13

Suspense

ローディングが終了したらそれに合わせて画面を書き換えなければいけません。つまり、fallbackの内容を片付けてサスペンドしたコンポーネントの本来の内容を表示するという作業が必要なはずです。サスペンド解除時はサスペンドしたSuspenseの中身が再レンダリングされる

下記のAlwaysSuspendは再レンダリングしたらまた新たなPromiseをthrowします。
これは、reactのSuspenseにてサスペンドされたAlwaysSuspendが解決した時再度描画されるため。

 const AlwaysSuspend: React.FC = () => {
   console.log("AlwaysSuspend is rendered");
   throw sleep(1000);
 };

https://zenn.dev/uhyo/books/react-concurrent-handson

expsh13expsh13

Transition

このようにstartTransitionの中で行われたステート更新はトランジションであると見なされます。トランジションは、端的に言えば優先度の低いステート更新です。

トランジションとしてマークされたステート更新は優先度が低いので、トランジション(としてマークされたステート更新によって引き起こされた再レンダリング)は中止されて一旦後回しにされる可能性があります。

const [counter, setCounter] = useState(0);
  return (
    <div className="text-center">
      <h1 className="text-2xl">React App!</h1>
      <Suspense fallback={<p>Loading...</p>}>
        <ShowData dataKey={counter} />
      </Suspense>
      <p>
        <button
          className="border p-1"
          onClick={() => {
            startTransition(() => {
              setCounter(counter + 1);
            });
          }}
        >
          Counter is {counter}
        </button>
      </p>
    </div>
  );

「Counter is 0」ボタンを押してみましょう。すると、直後は画面が何も反応しないはずです。1秒経つとShowDataの中身が「Data for 1 is …」に更新され、同時にボタンが「Counter is 1」になります。

つまり、ここでトランジションの結果として起こるレンダリングが遅延されたのです。遅延が起こった直接的な理由は言わずもがな、ShowDataがサスペンドしたことです。そして、トランジションの特異な点は、サスペンドの影響が<Suspense>の外まで漏れ出ている点にあります。というのも、counterはSuspenseの外にあるステートで、Suspenseの外でも使われているのに、そこのレンダリングも遅延されていますね。

https://zenn.dev/uhyo/books/react-concurrent-handson-2

expsh13expsh13

startTransition引数

scope: 1 つ以上の set 関数を呼び出して state を更新する関数。React は引数なしで直ちに scope を呼び出し、scope 関数呼び出し中に同期的にスケジュールされたすべての state 更新をトランジションとしてマークします。このような更新はノンブロッキングになり、不要なローディングインジケータを表示しないようになります。

https://ja.react.dev/reference/react/useTransition

expsh13expsh13

Server Actions

  • サーバアクションはトランジションの中で呼び出すようにしてください。サーバアクションが <form action> または formAction に渡される場合、自動的にトランジション内で呼び出されます。
  • サーバアクションは、サーバ側の状態を書き換える、更新目的のために設計されています。データの取得には推奨されません。したがって、サーバアクションを実装するフレームワークは通常、一度にひとつのアクションのみを処理し、返り値をキャッシュしないようにします。
import incrementLike from './actions';
import { useState, useTransition } from 'react';

function LikeButton() {
  const [isPending, startTransition] = useTransition();
  const [likeCount, setLikeCount] = useState(0);

  const onClick = () => {
    startTransition(async () => {
      const currentCount = await incrementLike();
      setLikeCount(currentCount);
    });
  };

  return (
    <>
      <p>Total Likes: {likeCount}</p>
      <button onClick={onClick} disabled={isPending}>Like</button>;
    </>
  );
}
// actions.js
'use server';

let likeCount = 0;
export default async function incrementLike() {
  likeCount++;
  return likeCount;
}

https://ja.react.dev/reference/rsc/use-server