📘

実務で役立つ「Fluent React」のエッセンス

に公開

O'Reillyから出版されているTejas Kumar氏著の「Fluent React」は、Reactの「使い方」だけでなく、「どのように動作するか」という内部メカニズムに深く踏み込んだ一冊です。Reactを日常的に業務で使用している開発者にとって、本書で解説されている概念やパターンを理解することは、より高速で、パフォーマンスが高く、直感的なWebアプリケーションを構築するための強力な武器となります。

この記事では、「Fluent React」の中から、特に実務で役立つと思われる重要なトピックをピックアップし、コード例を交えながら解説します。

1. パフォーマンス最適化の探求

Reactアプリケーションのパフォーマンスは常に重要な関心事です。Fluent Reactでは、その最適化手法について深く掘り下げています。

メモ化:React.memo, useMemo, useCallback

不必要な再レンダリングはパフォーマンス低下の主な原因です。Reactはメモ化のためのユーティリティを提供しています。

  • React.memo: コンポーネントのpropsが変更されない限り再レンダリングをスキップします。特に、計算コストの高いコンポーネントや、頻繁に再レンダリングされるコンポーネントの親を持つ場合に有効です。

    // ItemListコンポーネントをメモ化
    const MemoizedItemList = React.memo(function ItemList({ items }) {
      console.log("ItemList rendering...");
      return (
        <ul>
          {items.map((item) => (
            <li key={item.id}>{item.name}</li>
          ))}
        </ul>
      );
    });
    
    function App() {
      const [items, setItems] = React.useState([{ id: 1, name: "Apple" }]);
      const [count, setCount] = React.useState(0);
    
      // countが変わっても、itemsが変わらなければMemoizedItemListは再レンダリングされない
      return (
        <div>
          <button onClick={() => setCount(c => c + 1)}>Increment Count: {count}</button>
          <MemoizedItemList items={items} />
        </div>
      );
    }
    
  • useMemo: 計算結果をメモ化します。重い計算処理や、参照の同一性を保ちたいオブジェクト/配列の生成に使用します。

    function calculateExpensiveValue(a, b) {
      // ...重い計算
      console.log("Calculating expensive value...");
      let sum = 0;
      for (let i = 0; i < 100000000; i++) { // 意図的に重くする
          sum += a + b;
      }
      return sum / 100000000;
    }
    
    function MyComponent({ a, b }) {
      // aかbが変わらない限り、calculateExpensiveValueは再実行されない
      const expensiveValue = React.useMemo(() => calculateExpensiveValue(a, b), [a, b]);
    
      return <div>Computed Value: {expensiveValue}</div>;
    }
    
    function App() {
        const [valA, setValA] = useState(1);
        const [valB, setValB] = useState(2);
        const [otherState, setOtherState] = useState(0);
    
        return (
            <div>
                <MyComponent a={valA} b={valB} />
                <button onClick={() => setValA(a => a + 1)}>Change A</button>
                <button onClick={() => setValB(b => b + 1)}>Change B</button>
                <hr />
                {/* このボタンを押してもMyComponentの重い計算は走らない */}
                <button onClick={() => setOtherState(s => s + 1)}>
                  Update Other State: {otherState}
                </button>
            </div>
        );
    }
    
  • useCallback: 関数自体をメモ化します。React.memo でラップされた子コンポーネントに関数を渡す際、不要な再レンダリングを防ぐために使用します。

    const MemoizedButton = React.memo(({ onClick, children }) => {
      console.log(`Rendering ${children}`);
      return <button onClick={onClick}>{children}</button>;
    });
    
    function App() {
      const [count1, setCount1] = React.useState(0);
      const [count2, setCount2] = React.useState(0);
    
      // increment1は依存配列が空なので初回のみ生成され、以降同じインスタンス
      const increment1 = React.useCallback(() => {
        setCount1(c => c + 1);
      }, []);
    
      // increment2はAppが再レンダリングされるたびに新しい関数インスタンスが生成される
      const increment2 = () => {
        setCount2(c => c + 1);
      };
    
      return (
        <div>
          <p>Count 1: {count1}</p>
          <p>Count 2: {count2}</p>
          {/* count2が更新されても、increment1が変わらないのでButton 1は再レンダリングされない */}
          <MemoizedButton onClick={increment1}>Button 1 (useCallback)</MemoizedButton>
          {/* count1が更新されても、毎回新しいincrement2が渡されるのでButton 2は再レンダリングされる */}
          <MemoizedButton onClick={increment2}>Button 2 (No useCallback)</MemoizedButton>
           {/* このボタンは両方のボタンを再レンダリングさせない */}
           <button onClick={()=>{}}>Force Re-render Parent</button>
        </div>
      );
    }
    

注意点: メモ化は万能薬ではなく、過度な使用は逆にコードを複雑にし、わずかながらオーバーヘッドも生じます。プロファイリングを行い、ボトルネックとなっている箇所に適用することが重要です。特に、単純な計算や常に新しいインスタンスが生成されても問題ないケース(例:ネイティブHTML要素へのイベントハンドラ)では不要なことが多いです。

Lazy Loading と Suspense

初期ロード時のバンドルサイズを削減し、表示速度を改善するテクニックです。

  • React.lazy: コンポーネントが実際にレンダリングされるまで、そのコンポーネントのコードの読み込みを遅延させます。
  • Suspense: React.lazy で読み込んでいるコンポーネントが準備できるまでの間、フォールバックUI(ローディング表示など)を表示します。
import React, { Suspense, lazy } from 'react';

// HeavyComponent.js が初期バンドルに含まれず、必要になった時にロードされる
const HeavyComponent = lazy(() => import('./HeavyComponent'));
// フォールバック用の軽量コンポーネント
const FakeSidebarShell = () => <div style={{ width: '200px', border: '1px solid #ccc', padding: '10px'}}>Loading Sidebar...</div>;

function App() {
  const [show, setShow] = React.useState(false);

  return (
    <div>
      <button onClick={() => setShow(s => !s)}>Toggle Heavy Component</button>
      {/* HeavyComponentがロードされるまでfallbackが表示される */}
      <Suspense fallback={<FakeSidebarShell />}>
        {show && <HeavyComponent />}
      </Suspense>
    </div>
  );
}

2. 状態管理とコンポーネント設計パターン

アプリケーションが複雑になるにつれて、状態管理やコンポーネントの構成方法が重要になります。

useState vs useReducer

  • useState: シンプルな状態(単一の値、単純なオブジェクト/配列)に適しています。
  • useReducer: 複数の値が関連する複雑な状態、状態遷移ロジックが複雑な場合、状態更新ロジックをコンポーネントから分離してテストしやすくしたい場合に有効です。
// useReducerの例
const initialState = { count: 0, step: 1 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + state.step };
    case 'decrement':
      return { ...state, count: state.count - state.step };
    case 'setStep':
      // 不正な値が入らないようにバリデーションする例
      const newStep = Number(action.payload);
      return { ...state, step: isNaN(newStep) || newStep < 1 ? 1 : newStep };
    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
}

function Counter() {
  const [state, dispatch] = React.useReducer(reducer, initialState);

  return (
    <>
      Count: {state.count} (Step: {state.step})
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <label>
        Step:
        <input
          type="number"
          value={state.step}
          onChange={(e) => dispatch({ type: 'setStep', payload: e.target.value })}
          min="1"
        />
      </label>
    </>
  );
}

useReducer は状態更新ロジック(reducer関数)をコンポーネントの外に切り出せるため、テスト容易性が向上し、複雑な状態遷移をより管理しやすくなります。

設計パターン

Fluent Reactでは、Reactコミュニティで長年議論されてきた様々な設計パターンにも触れています。考え方を理解しておくことは有用です。

  • Presentational/Container Components: UI表示責務(Presentational)とロジック/状態管理責務(Container)を分離するパターン。フックによりContainerの役割はカスタムフックで代替されることが多いですが、関心の分離という原則は重要です。

  • Higher-Order Components (HOC): コンポーネントを引数に取り、機能を追加した新しいコンポーネントを返す関数。ロジックの再利用に使われますが、フックの方がシンプルになる場合が多いです。React.memo もHOCの一種です。書籍ではデータローディング状態を注入する withAsync HOCが例示されています。

    // HOCファクトリ
    const withAsync = (WrappedComponent) => {
      return function WithAsyncComponent({ isLoading, error, ...props }) {
        if (isLoading) {
          return <div>Loading...</div>;
        }
        if (error) {
          return <div>Error: {error.message}</div>;
        }
        // isLoading, error以外のpropsを元のコンポーネントに渡す
        return <WrappedComponent {...props} />;
      };
    };
    
    // 基本的なリスト表示コンポーネント
    function BasicTodoList({ data }) {
      return <ul>{data.map(item => <li key={item.id}>{item.text}</li>)}</ul>;
    }
    
    // HOCでラップ
    const TodoListWithAsync = withAsync(BasicTodoList);
    
    function App() {
      const [data, setData] = useState([]);
      const [isLoading, setIsLoading] = useState(true);
      const [error, setError] = useState(null);
    
      useEffect(() => {
        fetch("/api/todos")
          .then(res => res.json())
          .then(setData)
          .catch(setError)
          .finally(() => setIsLoading(false));
      }, []);
    
      // ラップされたコンポーネントに必要なpropsを渡す
      return <TodoListWithAsync isLoading={isLoading} error={error} data={data} />;
    }
    
  • Render Props: コンポーネントが「何をレンダリングするか」を決定する関数をpropとして受け取るパターン。ロジック共有に使われますが、ネストが深くなることがあります。

    import React, { useState, useEffect } from 'react';
    
    // ウィンドウサイズを提供するコンポーネント
    function WindowSize({ render }) { // 'render' propで関数を受け取る
      const [size, setSize] = useState({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    
      useEffect(() => {
        const handleResize = () => {
          setSize({ width: window.innerWidth, height: window.innerHeight });
        };
        window.addEventListener('resize', handleResize);
        return () => window.removeEventListener('resize', handleResize);
      }, []);
    
      // 受け取ったrender関数を呼び出し、サイズを渡す
      return render(size);
    }
    
    // 使用例
    function App() {
      return (
        <WindowSize render={({ width, height }) => (
          <div>Window is {width}x{height}px</div>
        )} />
      );
    }
    

    Children as a Function 形式も同様にロジックを共有しつつ描画を委譲します。

  • Compound Components: 複数のコンポーネントが協調して一つのUI部品を構成するパターン(例:<select><option>)。内部でContext APIを使い暗黙的に状態を共有します。UIライブラリでよく見られます。

    import React, { useState, useContext, createContext } from 'react';
    
    const AccordionContext = createContext({
      activeItemIndex: 0,
      setActiveItemIndex: (index) => {},
    });
    
    function Accordion({ children }) {
      const [activeItemIndex, setActiveItemIndex] = useState(0);
      const value = { activeItemIndex, setActiveItemIndex };
      return (
        <AccordionContext.Provider value={value}>
          <ul>{children}</ul>
        </AccordionContext.Provider>
      );
    }
    
    function AccordionItem({ children, index, title }) {
      const { activeItemIndex, setActiveItemIndex } = useContext(AccordionContext);
      const isActive = index === activeItemIndex;
      return (
        <li onClick={() => setActiveItemIndex(index)}>
          <strong>{title}</strong>
          {isActive && <div>{children}</div>}
        </li>
      );
    }
    
    // 使い方
    function App() {
      return (
        <Accordion>
          <AccordionItem index={0} title="Section 1">Content 1</AccordionItem>
          <AccordionItem index={1} title="Section 2">Content 2</AccordionItem>
        </Accordion>
      );
    }
    
  • State Reducer: コンポーネントが内部の状態更新ロジックを外部から受け取ったreducer関数で上書き/拡張できるようにするパターン。コンポーネントのカスタマイズ性を高めます。

    import React, { useReducer } from 'react';
    
    function internalToggleReducer(state, action) {
      switch (action.type) {
        case 'TOGGLE': return { on: !state.on };
        default: throw new Error(`Unhandled action type: ${action.type}`);
      }
    }
    
    const defaultStateReducer = (state, action) => action.changes;
    
    function Toggle({
      initialOn = false,
      stateReducer = defaultStateReducer
    }) {
      const [state, dispatch] = useReducer(
        (state, action) => {
          const changes = internalToggleReducer(state, action);
          // 外部Reducerに変更を委譲
          return stateReducer(state, { ...action, changes });
        },
        { on: initialOn }
      );
      const { on } = state;
      const handleToggle = () => dispatch({ type: 'TOGGLE' });
      return <button onClick={handleToggle}>{on ? 'On' : 'Off'}</button>;
    }
    
    // --- 使用例 ---
    // 特定の条件で状態変更をキャンセルするReducer
    function preventOffReducer(state, action) {
      if (action.type === 'TOGGLE' && action.changes.on === false) {
         console.log("Preventing turning off!");
         return state; // 変更をキャンセル
      }
      return action.changes; // それ以外は内部Reducerの結果に従う
    }
    
    function App() {
      return <Toggle stateReducer={preventOffReducer} />;
    }
    

3. Reactの内部動作:Virtual DOM と Reconciliation

ReactがどのようにUIを効率的に更新しているのか、その心臓部を理解します。

  • Virtual DOM (vDOM): メモリ上に保持されるUIの状態を表す軽量なJavaScriptオブジェクトツリー。実際のDOM操作を最小限に抑えるための重要な概念です。
  • Reconciliation (調整): stateやpropsの変更があった際に、Reactが新しいVirtual DOMツリーと前回のツリーを比較し、差分(diff)を検出するプロセス。
  • Diffing Algorithm: 効率的に差分を見つけるためのアルゴリズム。Reactはいくつかのヒューリスティック(経験則)を用いてこれを高速化しています(例:要素タイプが異なればツリー全体を再構築、key propによる要素の特定)。
  • Fiber Reconciler: React 16で導入された新しい調整アルゴリズム。レンダリング作業を小さな単位(Fiber)に分割し、中断・再開・優先度付けを可能にしました。これにより、Concurrent Reactが実現されています。

これらの内部動作を理解することで、「なぜkey propが必要なのか」「なぜReact.memoが効くのか/効かないのか」「バッチングがどのように機能するのか」といった疑問に対する答えが見えてきます。

4. モダンReact:Concurrent React と Server Components

Reactは進化を続けており、新しいパラダイムが登場しています。

Concurrent React

Fiber Reconcilerによって可能になった、レンダリングを中断したり、優先度をつけたりする機能群。UIの応答性を劇的に向上させます。

  • useTransition: state更新を「トランジション(遷移)」としてマークし、優先度を下げます。これにより、例えば入力フィールドへの応答のような緊急性の高い更新が、データのフェッチや重いUIのレンダリングのような緊急性の低い更新によってブロックされるのを防ぎます。isPending フラグを提供し、トランジション中のローディング状態を示すことができます。

    import React, { useState, useTransition } from 'react';
    
    function TabContainer() {
      const [tab, setTab] = useState('about');
      // isPending: トランジション中かどうかのフラグ
      // startTransition: 低優先度更新をラップする関数
      const [isPending, startTransition] = useTransition();
    
      function selectTab(nextTab) {
        // startTransitionでラップすることで、タブ切り替えに伴う
        // コンポーネントのレンダリングが他の緊急性の高い更新をブロックしない
        startTransition(() => {
          setTab(nextTab);
        });
      }
    
      return (
        <>
          <button onClick={() => selectTab('about')} disabled={tab === 'about'}>About</button>
          <button onClick={() => selectTab('posts')} disabled={tab === 'posts'}>Posts (slow)</button>
          <button onClick={() => selectTab('contact')} disabled={tab === 'contact'}>Contact</button>
          <hr />
          {/* isPending中はローディング表示 */}
          {isPending && "Loading..."}
          {!isPending && tab === 'about' && <AboutTab />}
          {!isPending && tab === 'posts' && <PostsTab />} {/* PostsTabは重いとする */}
          {!isPending && tab === 'contact' && <ContactTab />}
        </>
      );
    }
    
  • useDeferredValue: 値の更新を遅延させるフック。useTransitionがstate更新の優先度を下げるのに対し、こちらは渡された値の更新自体を遅らせます。Reactはまず古い値で再レンダリングし、緊急性の高い処理が終わった後、新しい値で再度レンダリングします。これにより、例えば検索候補のリスト表示のような、入力中に頻繁に更新されるが即時性は求められないUI部分のレンダリングを遅延させ、入力自体の応答性を維持します。

    import React, { useState, useDeferredValue, useMemo } from 'react';
    
    // 何千ものアイテムを持つリストをフィルタリングする重いコンポーネント
    function SearchResults({ query }) {
       // queryの更新を遅延させる。入力中は古いqueryが使われ、入力が止まると新しいqueryになる
      const deferredQuery = useDeferredValue(query);
    
      const filteredItems = useMemo(() => {
        console.log(`Filtering for: ${deferredQuery}`);
        // ここで実際に重いフィルタリング処理を行う (例)
        const items = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`);
        if (!deferredQuery) return items.slice(0, 10); // 最初は少しだけ表示
        return items.filter(item => item.toLowerCase().includes(deferredQuery.toLowerCase())).slice(0, 10);
      }, [deferredQuery]); // deferredQueryが変わった時だけフィルタリング
    
      return (
        <ul>
          {filteredItems.map(item => <li key={item}>{item}</li>)}
        </ul>
      );
    }
    
    function App() {
      const [query, setQuery] = useState('');
      return (
        <div>
          <input
            type="text"
            placeholder="Search..."
            value={query}
            onChange={e => setQuery(e.target.value)}
            style={{ width: '100%', padding: '10px', marginBottom: '10px' }}
          />
          {/* 入力自体は遅延しないが、SearchResultsの更新は遅延する */}
          <SearchResults query={query} />
        </div>
      );
    }
    
  • Tearing問題と useSyncExternalStore: Concurrent Renderingでは、レンダリングが中断・再開されるため、同じレンダリングパス内でも外部ストア(Redux, Zustand, グローバル変数など)から読み取る値が異なる可能性があります。これを**Tearing(引き裂き)**と呼びます。useSyncExternalStoreは、外部ストアへのサブスクライブとそのストアから値を取得する関数を受け取り、Reactのレンダリングと同期された一貫性のある値を保証します。これにより、Concurrent Rendering中でも安全に外部ストアを利用できます。

    import React, { useSyncExternalStore } from 'react';
    import { myExternalStore } from './myExternalStore'; // 例: { subscribe, getSnapshot } を持つオブジェクト
    
    function MyComponent() {
      // ストアの現在の値を安全に読み取る
      // ストアが更新されると、コンポーネントは強制的に再レンダリングされる
      const storeValue = useSyncExternalStore(
        myExternalStore.subscribe, // 購読するための関数
        myExternalStore.getSnapshot // 現在の値を取得する関数
      );
    
      return <div>External Store Value: {storeValue}</div>;
    }
    
    // --- myExternalStore.js (簡単な例) ---
    let value = 0;
    const listeners = new Set();
    setInterval(() => {
        value++;
        listeners.forEach(listener => listener());
    }, 1000);
    
    export const myExternalStore = {
        subscribe(listener) {
            listeners.add(listener);
            return () => listeners.delete(listener); // クリーンアップ関数
        },
        getSnapshot() {
            return value;
        }
    };
    

React Server Components (RSCs)

サーバーサイドでのみレンダリングされる新しいタイプのコンポーネント。

  • 概念と利点: クライアントサイドのJavaScriptバンドルからコンポーネントコードを完全に削除し、サーバーの能力(データアクセス、計算能力)を最大限に活用します。これにより、初期ロードパフォーマンスの向上、バンドルサイズの削減、データフェッチの簡素化、セキュリティの向上が期待できます。

  • サーバーでのレンダリングプロセス:

    1. RSCを含むReactツリーがサーバー上でレンダリングされます。
    2. RSC関数が実行され、データフェッチ(async/await可能)などが行われます。
    3. 結果として、React要素(vDOM)のツリーが生成されます。ただし、Client Componentの部分は特別な参照(後述)に置き換えられます。
    4. このvDOMツリーは、特別なシリアライズ形式(通常JSONではない、ストリーミング可能な形式)でクライアントに送信されます。
     // サーバーサイドでの概念的な処理 (Fluent React P.255参考)
     async function turnServerComponentsIntoSpecialFormat(jsx) {
       // JSXを再帰的に処理
       if (typeof jsx.type === 'function') {
         if (/* is RSC? (e.g., not marked with "use client") */) {
           const Component = jsx.type;
           const props = jsx.props;
           // RSCはasyncかもしれないのでawait
           const renderedElement = await Component(props);
           // 再帰的に処理
           return await turnServerComponentsIntoSpecialFormat(renderedElement);
         } else {
           // Client Componentの場合
           return {
             $$typeof: Symbol.for('react.client.reference'), // 特別なマーカー
             $$id: './path/to/ClientComponent.js#default', // バンドラーが解決するID
             props: jsx.props // propsは渡す
           };
         }
       } else if (typeof jsx.type === 'string') {
          // ネイティブHTML要素の場合、propsを再帰処理
         const processedProps = {};
         for(const key in jsx.props) {
             processedProps[key] = await turnServerComponentsIntoSpecialFormat(jsx.props[key]);
         }
         return { ...jsx, props: processedProps };
       }
       // ... 他のケース(配列、プリミティブなど)
       return jsx;
     }
     const specialFormat = await turnServerComponentsIntoSpecialFormat(<App />);
     // このspecialFormatをクライアントに送信
    
  • クライアントでの処理:

    1. クライアントはサーバーから送信された特別な形式を受け取ります。
    2. React(クライアントサイド)はこの形式を解析します。
    3. 通常のHTML要素やテキストはそのままDOMになります。
    4. Client Componentの参照が見つかると、対応するJavaScriptコード(初期バンドルまたは遅延ロードされたもの)をロードし、propsを渡してクライアントサイドでレンダリング(ハイドレーションまたは新規レンダリング)します。
  • Client Components ("use client"): state (useState, useReducer)、ライフサイクル/effect (useEffect)、ブラウザAPIへのアクセス、イベントリスナーなど、インタラクティブな機能を持つコンポーネントはClient Componentとしてマークする必要があります。

  • Server ComponentsからClient Componentsへのデータフロー: RSCはClient Componentを直接インポートできません(バンドルに含まれないため)。代わりに、RSCはClient Componentを子要素として(またはpropsとして)レンダリングします。データはpropsを通じてRSCからClient Componentへ渡されます。

    // app/page.tsx (RSC)
    import MyClientComponent from './MyClientComponent';
    import ServerDataComponent from './ServerDataComponent';
    
    export default function Page() {
      const serverMessage = "Data from server";
      return (
        <div>
          <h1>Server Component Page</h1>
          {/* Client Componentを子として、またはpropsでラップ */}
          <MyClientComponent initialMessage={serverMessage}>
             {/* RSCをchildrenとして渡すことは可能 */}
            <ServerDataComponent />
          </MyClientComponent>
        </div>
      );
    }
    
    // app/MyClientComponent.tsx
    "use client";
    import React, { useState } from 'react';
    
    export default function MyClientComponent({ initialMessage, children }) {
      const [message, setMessage] = useState(initialMessage);
      return (
        <div>
          <h2>Client Component</h2>
          <p>{message}</p>
          <button onClick={() => setMessage("Updated on client")}>Update</button>
          {/* RSCから渡されたchildren(RSC自体を含む)を表示 */}
          {children}
        </div>
      );
    }
    
     // app/ServerDataComponent.tsx (RSC)
    export default async function ServerDataComponent() {
        await new Promise(res => setTimeout(res, 1000)); // データフェッチをシミュレート
        return <p>Some async data loaded on server!</p>;
    }
    
  • Server Actions: RSCの重要な機能。"use server"; ディレクティブを関数またはモジュールの先頭に記述することで、その関数がクライアントから呼び出されてもサーバーサイドで実行されることを示します。フォームのaction属性やイベントハンドラに渡すことで、クライアントサイドJSなしでデータのミューテーション(作成、更新、削除)を実行できます。

    // app/actions.js
    "use server"; // モジュール全体をServer Actionsにする
    
    import { db } from './db';
    import { revalidatePath } from 'next/cache'; // Next.jsの場合
    
    export async function createTodo(formData) {
      const todoText = formData.get('todo');
      if (!todoText) return { error: 'Todo text cannot be empty' };
      try {
        await db.addTodo({ text: todoText });
        revalidatePath('/todos'); // 関連ページのキャッシュをクリア
        return { success: true };
      } catch (e) {
        return { error: 'Failed to create todo' };
      }
    }
    
    // app/AddTodoForm.tsx
    "use client";
    import { createTodo } from './actions';
    import { useFormState, useFormStatus } from 'react-dom'; // React DOMのフック
    
    const initialState = { error: null, success: false };
    
    function SubmitButton() {
        const { pending } = useFormStatus(); // フォーム送信状態を取得
        return <button type="submit" disabled={pending}>{pending ? 'Adding...' : 'Add Todo'}</button>;
    }
    
    export default function AddTodoForm() {
      // useFormStateでServer Actionの結果と保留状態を管理
      const [state, formAction] = useFormState(createTodo, initialState);
    
      return (
        <form action={formAction}>
          <input type="text" name="todo" placeholder="New Todo" />
          <SubmitButton />
          {state?.error && <p style={{ color: 'red' }}>{state.error}</p>}
          {state?.success && <p style={{ color: 'green' }}>Todo added!</p>}
        </form>
      );
    }
    
  • RSCのルールと制約:

    • シリアライズ可能性: RSCからClient Componentへ渡されるpropsはシリアライズ可能でなければなりません(関数やDateオブジェクトなどは渡せない)。
    • No State/Effects: RSCはuseState, useEffect, useReducerなどのフックを使用できません。
    • No Browser APIs: RSCはwindow, documentなどのブラウザ固有APIにアクセスできません。
    • Client Imports Server: Client ComponentはRSCを直接importできません。

RSCsはReactアプリケーションのアーキテクチャに大きな変化をもたらし、パフォーマンスと開発体験の両方を向上させる可能性を秘めています。フレームワーク(特にNext.js App Router)を通じて利用するのが一般的です。

まとめ

Fluent React」は、Reactの内部動作や設計思想を深く理解するための優れたガイドブックです。本書で解説されているメモ化、状態管理パターン、Virtual DOM、Reconciliation、Concurrent React、Server Componentsなどの知識は、Reactを業務で使う上で、よりパフォーマンスが高く、メンテナンスしやすく、洗練されたコードを書くための基盤となります。

ここで紹介したのは本書のほんの一部です。本書は、Reactの「なぜ」を探求し、より深いレベルでReactを理解したい開発者にとって、非常に価値のあるリソースとなるでしょう。ぜひ実際に手に取り、Reactの奥深い世界を探求してみてください。日々の開発がより効率的で、楽しくなるはずです。


Discussion