React 19が来る

6 min read

React19 Betaがnpmで利用可能になったようです。

Actions

慣習で非同期遷移(async transitions)を使用する関数は "Actions" と呼ばれるとのこと。 Actionはデータの送信を自動的に管理してくれる。

  • 保留状態
  • 楽観的更新
  • エラー処理
  • フォーム

useTransition()

よくある保留状態とかエラー状態を useState() で処理しているコードがある。

const [name, setName] = useState("");
const [error, setError] = useState(null);
const [isPending, setIsPending] = useState(false);

const handleSubmit = async () => {
  setIsPending(true);
  const error = await updateName(name);
  setIsPending(false);
  if (error) {
    setError(error);
    return;
  } 
  redirect("/path");
};

これがReact19ではtransitionで非同期関数を使って、保留中のステート、エラー、フォーム、楽観的な更新を自動的に処理できるようになる。 上記のコードを useTransition() を使うと以下のようにpending状態を処理できる。

const [name, setName] = useState("");
const [error, setError] = useState(null);
const [isPending, startTransition] = useTransition();

const handleSubmit = () => {
  startTransition(async () => {
    const error = await updateName(name);
    if (error) {
      setError(error);
      return;
    } 
    redirect("/path");
  })
};

非同期トランジション(async transition)はすぐにisPendingをtrueに設定し、非同期リクエストを行い、transition後にisPendingをfalseに切り替える。 フレームワーク側で制御してくれるのは何となく安心感があるかも。

New hook: useActionState

Actionsでよくあるケースを簡単にするために、useActionStateという新しいフックを追加された。

const [error, submitAction, isPending] = useActionState(
  async (previousState, newName) => {
    const error = await updateName(newName);
    if (error) {
      // Actionの結果は何でも返すことができる。
      // ここではエラーだけを返す。
      return error;
    }

    // 正常系
    return null;
  },
  null,
);
// Using <form> Actions and useActionState
function ChangeName({ name, setName }) {
  const [error, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      const error = await updateName(formData.get("name"));
      if (error) {
        return error;
      }
      redirect("/path");
      return null;
    },
    null,
  );

  return (
    <form action={submitAction}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>Update</button>
      {error && <p>{error}</p>}
    </form>
  );
}

React.useActionStateは、CanaryリリースではReactDOM.useFormStateだったが、名前を変更し、useFormStateを非推奨としたとのこと。 useActionStateの詳細についてはドキュメントを参照。

React DOM: form Actions

ActionsはReact 19のreact-domの新しい<form>機能とも統合されている。 <form>, <input>, <button>要素のactionとformAction propsに関数を渡すことで、Actionを使って自動的にフォームを送信できるようになった。

<form action={actionFunction}>

<form>Actionが成功すると、Reactは制御されていないコンポーネントのフォームを自動的にリセット。<form>を手動でリセットする必要がある場合は、requestFormReset React DOM APIを呼び出すことができる。 詳細は <form><input><button>のドキュメントを参照。

React DOM: New hook: useFormStatus

デザインシステムにおいて、デザインコンポーネントを書くときに、そのコンポーネントの中にある <form>に関する情報にアクセスする必要がある。これはContextを使って行うことができるが、よくあるケースを簡単にするために、新しいhookであるuseFormStatusが追加された。

import {useFormStatus} from 'react-dom';

function DesignButton() {
  const { pending } = useFormStatus();
  return <button type="submit" disabled={pending} />
}

useFormStatusは親の<form>のステータスをContextのProvierかのように読み込んでくれるらしい! 便利そう。 詳細はドキュメントで。

New hook: useOptimistic

データミューテーションを実行するときのもう1つの一般的なUIパターンは、非同期リクエストの実行中に最終状態を楽観的に表示すること。React19ではこれを簡単にするためにuseOptimisticという新しいhookが追加されている。

function ChangeName({currentName, onUpdateName}) {
  const [optimisticName, setOptimisticName] = useOptimistic(currentName); // <--

  const submitAction = async formData => {
    const newName = formData.get("name");
    setOptimisticName(newName); // <--
    const updatedName = await updateName(newName);
    onUpdateName(updatedName);
  };

  return (
    <form action={submitAction}>
      <p>Your name is: {optimisticName}</p> // <--
      <p>
        <label>Change Name:</label>
        <input
          type="text"
          name="name"
          disabled={currentName !== optimisticName} // <--
        />
      </p>
    </form>
  );
}

useOptimisticフックは、updateNameリクエストが進行している間、直ちにoptimisticNameをレンダリングする。更新が終了するかエラーが発生すると、Reactは自動的にcurrentNameの値に切り替える。 詳細はドキュメント

New API: use

React 19では、レンダリングでリソースを読み込むための新しいAPIを導入された。 useを使ってプロミスを読み込むと、Reactはプロミスが解決するまでSuspendする。

import {use} from 'react'; // <--

function Comments({commentsPromise}) {
  // useはPromiseが解決するまでsuspendする。
  const comments = use(commentsPromise); // <--
  return comments.map(comment => <p key={comment.id}>{comment}</p>);
}

function Page({commentsPromise}) {
  // Commentsで`use`がサスペンドすると、このSuspense boundaryが表示される
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Comments commentsPromise={commentsPromise} />
    </Suspense>
  )
}

useはrenderで作成されたプロミスをサポートしていない。renderで作成されたプロミスをuseに渡そうとすると、Reactは警告を発します

ここがいまいちわかってない。

Contextも読むことができるとのこと。 ドキュメント

React Server Components

Server Components

Server Componentsは、クライアントアプリケーションやSSRサーバーとは別の環境で、バンドルする前のコンポーネントを先にレンダリングできる新しいオプション。Server Componentsは、CIサーバー上でビルド時に一度だけ実行することも、Webサーバーを使ってリクエストごとに実行することも可能。

Server Actions

クライアントコンポーネントがサーバー上で実行される非同期関数を呼び出すことを可能にする。 server actionsが"use server"ディレクティブで定義されると、フレームワークは自動的にサーバー関数への参照を作成し、その参照をクライアントコンポーネントに渡します。その関数がクライアントで呼び出されると、Reactはサーバにリクエストを送信して関数を実行し、結果を返します。 これもNextjsのapp routerで先に使ってた。便利。

よくある誤解として、Server Componentsは "use server "で示されますが、Server Components用のディレクティブはありません。use server" ディレクティブは、サーバーアクションに使用されます。

React19の改善点(気になったところ抜粋)

ref as a prop

forwardRefを使わなくても良いようになった!

function MyInput({placeholder, ref}) {
  return <input placeholder={placeholder} ref={ref} />
}

//...
<MyInput ref={ref} />

<Context> as a provider

React 19では、<Context.Provider>の代わりに<Context>をプロバイダとしてレンダリングできる。将来的に<Context.Provider>は廃止されるとのこと。

const ThemeContext = createContext('');

function App({children}) {
  return (
    <ThemeContext value="dark">
      {children}
    </ThemeContext>
  );  
}

useDeferredValue initial value

useDeferredValueにinitialValueオプションを追加。

function Search({deferredValue}) {
  // On initial render the value is ''.
  // Then a re-render is scheduled with the deferredValue.
  const value = useDeferredValue(deferredValue, '');
  
  return (
    <Results query={value} />
  );
}

ドキュメント

Support for Document Metadata

React 19では、コンポーネント内のドキュメントメタデータタグをネイティブにレンダリングするためのサポートを追加されるとのこと。react-helmetとか使わなくても良くなる!ただreact-helmetのようなライブラリが必要なケースもあるかも。

Support for stylesheets

コンポーネントごとにlinkの形式でスタイルシートを呼び出せるようになった?

function ComponentOne() {
  return (
    <Suspense fallback="loading...">
      <link rel="stylesheet" href="foo" precedence="default" />
      <link rel="stylesheet" href="bar" precedence="high" />
      <article class="foo-class bar-class">
        {...}
      </article>
    </Suspense>
  )
}

function ComponentTwo() {
  return (
    <div>
      <p>{...}</p>
      <link rel="stylesheet" href="baz" precedence="default" />  <-- will be inserted between foo & bar
    </div>
  )
}

Support for async scripts

同様にscriptのレンダリングもできるようになったぽい。

function MyComponent() {
  return (
    <div>
      <script async={true} src="..." />
      Hello World
    </div>
  )
}

function App() {
  <html>
    <body>
      <MyComponent>
      ...
      <MyComponent> // won't lead to duplicate script in the DOM
    </body>
  </html>
}
Continue reading →

Software Design 2024年5月号を読んだ

5 min read

2024 年 5 月号は TypeScript 特集だったので気になってました。 正直未だに雰囲気で使ってるところもあったので、改めて情報を整理するいい機会になった。

widening

const user = { name: "foo", age: 25 };

上みたいなオブジェクトを定義したときにそれぞれのプロパティがリテラルじゃなくてプリミティブな型として推論されるのは知っていたけど、その型の拡大を「widening」と呼ぶとのこと。

satisfies 演算子

as const satisfiesを使うことによって推論結果を保持しつつ型を制限することができる。

satiesfies 演算子は TypeScript4.9 で導入されました。アノテーションと何が違うのかよくわかっていなくて積極的に使えてませんでしたが、アノテーションの違いとしては型推論がそのまま保持されることが便利ポイント。

こちらの記事も参考になりました。

Union 型

オブジェクトの型でオプショナルなプロパティを設定するときに、以下のようにプロパティ名の後ろに?を付けるパターンを使うか、Union 型を使うか迷うことがあったなと読んでいて思い出しました。

type Option1 = {
  foo?: string;
};

type Option2 = {
  foo: string | undefined;
};

私はわりと Option1 の書き方をしていることが多かったのですが、実際にオブジェクトを定義するときに書き忘れたのか意図して省略したのかが判別できないといった点を考慮できてなかったなと感じました。 Option2 の書き方でも良さそう。

Result 型

Rust を少し触ったときにも出てきた成功と失敗を表現するための Result 型を TypeScript で表現するとこう。

type Success<T> = { success: true; value: T };
type Failure<E> = { success: false; error: E };
type Result<T, E = unknown> = Success<T> | Failure<E>;

function readDate(): Result<string, Error> {
  if (Math.random() < 0.5) {
    return { success: true, value: "data" };
  } else {
    return { success: false, error: new Error("Failed to read data") };
  }
}

const result = readData();
if (result.success) {
  console.log(result.value);
} else {
  console.log(result.error);
}

今まで例外処理は try-catch だけ使っていて Result 型を使ってハンドリングしたことがなかったけど、こうみると便利そうに見えるし、型からも例外が起こり得るのだなと読み取れて良さそう。Result型用のライブラリもある。

構造的型付けとブランド型

型システムには「名前的型付け」と「構造的型付け」が存在して、TypeScriptは後者を採用している。TypeScriptが構造的型付けを採用した理由はダックタイピングやオブジェクトリテラルなどJavaScriptの特徴と相性が良いためだが、TypeScriptでも名前的型付けを実現する方法がある。

意図せずに型が互換性を持ってしまった場合に名前的型付けを実現することで区別できるが、その方法としてprivateプロパティを持つclassを使ったやり方とブランド型がある。デザインパターンとして存在しているのは理解したけど、個人的にはブランド型の型アサーションを使ったやり方に若干不安を感じた。

ブランド型を使った型の区別の例

type UserId = {
  __brand: "UserId";
  id: number;
}

type ProductId = {
  __brand: "ProductId";
  id: number;
}

const userId = { id: 1 } as UserId;

実践Mapped Types

普段Mapped Typesを使わずにアプリケーションのコードを書いてきたので難しめな印象を持っていたがたぶん理解できたと思う。

条件型

入力された型に対して別の型を返すことができる。 この三項演算子の形が入れ子になっていくとすごい読みづらい。。。

type R1 = true extends boolean ? 1 : 2;
// => 1

type F<T> = T extends { v: boolean } ? 1 : 2;

type R1 = F<{v: true}>;    // => 1
type R2 = F<{v: false}>;   // => 1
type R3 = F<{v: "foo"}>;   // => 2
type R4 = F<boolean>;      // => 2

inferによる型の取り出し

条件型の機能でinferキーワードと一緒に使うことで型に名前をつけて取り出すことができる。

type ValueType<T> = T extends { value: infer U } ? U : never;
type F = ValueType<{ value: number }>;
// => number

Mapped Types

オブジェクト型のキー初期化では、in演算子を使った特殊な宣言を行える。

type T = { a: number; b: string; };
type M = {
  [k in keyof T]: T[k]
}
// => { a: number; b: string; };

Pick2の実装

これらの機能を使ってPickと同じふるまいをするPick2の実装が以下。

type Pick2<T, K extends keyof T> {
  [P in K]: T[P]
}

type F = <Pick2<{a: number; b: string}, "b">>
// => { b: string };

IDE上で読みやすくするユーティリティ

type User = {
  id: string;
  name: string;
  createAt: number;
}

type Optional<T extends {}, K extends keyof T> = Omit<T, K> & {
  [k in K]?: T[k] | undefined;
}

type DraftUser = Optional<User, "id" | "createAt">;

インターセクションやユーティリティ型を含んだ複雑な型はIDE上だとLanguage Serverが定義されたそのままの方情報を保持しようとするため読みづらいが、Identifyユーティリティを定義することで最終的な型だけ取り出す事が可能。

type Identify<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
type DraftUser = Identify<Optional<User, "id" | "createAt">>

/*
{
    name: string;
    id?: string | undefined;
    createAt?: number | undefined;
}
*/

章の中だと以下の記載だったけど、上の方が正しい型になる。 in演算子のところがおそらく間違っている。

type Identify<T> = T extends infer U ? { [K in keyof U]: K } : never;
type DraftUser = Identify<Optional<User, "id" | "createAt">>

/*
{
    name: "name";
    id?: "id" | undefined;
    createAt?: "createAt" | undefined;
}
*/

Mapped Typesのパターン

type F<T extends {}> = {
    [k in keyof T]: T[k] extends string ? true : false;
}

type User = {
    id: number;
    name: string;
    createdAt: number;
}

type R1 = F<User>

/*
{
    id: false;
    name: true;
    createdAt: false;
}
*/

おわり

Mapped Types周りの知見がほとんどなかったのでキャッチアップできてよかった。ただ筆者の方も書いている通りそもそも難しかったり読み手の負担が増えることもあるので使い所は考えたほうが良さそう。特に条件型が入れ子になるようなコードの例を最初に見たときに数分固まってしまったので。

Continue reading →

ブログ開設

1 min read

Lumeを使ってブログをつくりました。

以前からZennやQiitaなどより気軽に思考を書き出せる場所が欲しいと思っていたのでつくってみました。今後はこちらに技術的なことやそれ以外についても気が向いたら書いていこうと思います。

今回初めてLumeを使いましたが、cms機能があってプレビュー画面を見ながら書けるので体験が良さげです。あとこのページでも使っているsimple-blogのテーマも気に入ってます。

Continue reading →

More posts can be found in the archive.