日本語版
最新ニュース
科学&テクノロジー

40行未満のコードで独自のReact状態管理ライブラリを構築する

React の状態管理ライブラリがどのように構築されているか疑問に思ったことはありませんか? 大量の定型文と大きなバンドル サイズを持つ redux のようなソリューションから、はるかに軽量でシンプルな zustand や jotai のようなライブラリまで、さまざまなものがあります。今日は独自の状態管理ライブラリを構築し、舞台裏で起こる魔法を見てみましょう。 useSyncExternalStore の理解 React 18では、新しいフックが導入されました。 同期外部ストアを使用する これにより、React は任意の外部ストアと同期できるようになります。 useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?) パラメータの内訳は次のとおりです。 購読する コールバックをパラメータとして受け取り、ストアの状態が変化したときに呼び出されるようにコールバックを外部ストアにサブスクライブするには、サブスクライブ解除関数を返す必要があります。 スナップショットを取得する ストアの現在のスナップショットを取得します。このスナップショットはキャッシュされた値である必要があります。Reactはレンダリングごとにこの値を比較するためです。 Object.is(getSnapshot(), oldSnapshot)毎回新しい値を指定すると、無限ループが発生します。 サーバースナップショットを取得する (オプション) サーバー上でレンダリングするときにスナップショットを返すことができます。これは、外部ストアまたはサブスクリプション ソースをサーバー上で実行できない場合や、サーバー上で実行するために特定の処理が必要な場合に役立ちます。 useSyncExternalStore を活用することで、要件に合わせた最小限のストアを構築できます。 なぜ React Context を使用しないのですか? リアクトコンテキスト…

40行未満のコードで独自のReact状態管理ライブラリを構築する

1723842117
2024-08-15 13:18:23

React の状態管理ライブラリがどのように構築されているか疑問に思ったことはありませんか? 大量の定型文と大きなバンドル サイズを持つ redux のようなソリューションから、はるかに軽量でシンプルな zustand や jotai のようなライブラリまで、さまざまなものがあります。今日は独自の状態管理ライブラリを構築し、舞台裏で起こる魔法を見てみましょう。

useSyncExternalStore の理解

React 18では、新しいフックが導入されました。 同期外部ストアを使用する これにより、React は任意の外部ストアと同期できるようになります。

useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)

パラメータの内訳は次のとおりです。

  • 購読する コールバックをパラメータとして受け取り、ストアの状態が変化したときに呼び出されるようにコールバックを外部ストアにサブスクライブするには、サブスクライブ解除関数を返す必要があります。
  • スナップショットを取得する ストアの現在のスナップショットを取得します。このスナップショットはキャッシュされた値である必要があります。Reactはレンダリングごとにこの値を比較するためです。 Object.is(getSnapshot(), oldSnapshot)毎回新しい値を指定すると、無限ループが発生します。
  • サーバースナップショットを取得する (オプション) サーバー上でレンダリングするときにスナップショットを返すことができます。これは、外部ストアまたはサブスクリプション ソースをサーバー上で実行できない場合や、サーバー上で実行するために特定の処理が必要な場合に役立ちます。

useSyncExternalStore を活用することで、要件に合わせた最小限のストアを構築できます。

なぜ React Context を使用しないのですか?

リアクトコンテキスト は、コンポーネントがその下にあるコンポーネント ツリー全体に props を渡すことができるようにする React の機能です。つまり、ストアとして使用でき、実行可能なオプションです。

React コンテキストには定型文が必要です:

const context = createContext();

const CountProvider = ({ children }) => {
  const [count, setCount] = useState(0);
  return context.Provider value={{ count, setCount }}>{children}context.Provider>;
};

export function App() {
  return (
    CountProvider>
      Outer />
      Other />
    CountProvider>
  );
}

コンテキストを多用すると、「コンテキスト ヘル」に陥る可能性があります。これは、多くのコンテキスト プロバイダーが App コンポーネント内にネストされる状態です。

export function App() {
  return (
    CountProvider>
      AuthProvider>
        ThemeProvider>
          CacheProvider>
            IntlProvider>
              TooltipProvider>
                UserSettingsProvider>
                  NotificationProvider>
                    AnalyticsProvider>
                      Content />
                    UserSettingsProvider>
                  NotificationProvider>
                AnalyticsProvider>
              TooltipProvider>
            IntlProvider>
          CacheProvider>
        ThemeProvider>
      AuthProvider>
    CountProvider>
  );
}

さらに、コンテキストを使用すると、以下に示すように、コンポーネント ツリー全体の再レンダリングが誤ってトリガーされる可能性があります。

export function App() {
  const [count, setCount] = useState(0);

  return (
    context.Provider value={{ count, setCount }}>
      Outer />
      Other />
    context.Provider>
  );
}

コンテキストのコンシューマーから setCount を使用すると、状態が App コンポーネントに設定され、再レンダリングされると、そのすべての子コンポーネントも再レンダリングされるため、アプリ全体が再レンダリングされます (Outer と Other の両方が再レンダリングされます)。

また、外部ストアを使用すると、React を http リクエストなどの外部システムとより簡単に同期できるようになります。コンテキストでは useEffect を使用することになりますが、外部ストアではストアを直接更新するだけで、サブスクライブしているコンポーネントに変更が反映されます。

店舗の構築

ストアの実装について詳しく見ていきましょう。まずは基本構造から始めて、要件に応じて徐々に強化していきます。

import { useSyncExternalStore } from 'react';

export type Listener = () => void;

function createStoreT>({ initialState }: { initialState: T }) {
  let subscribers: Listener[] = [];
  let state = initialState;

  const notifyStateChanged = () => {
    subscribers.forEach((fn) => fn());
  };

  return {
    subscribe(fn: Listener) {
      subscribers.push(fn);
      return () => {
        subscribers = subscribers.filter((listener) => listener !== fn);
      };
    },
    getSnapshot() {
      return state;
    },
    setState(newState: T) {
      state = newState;
      notifyStateChanged();
    },
  };
}

購読者 ストアの状態が変化するたびにストアが通知するリスナーの配列です。
これは、setState が呼び出されたときに更新され、ストアのすべてのサブスクライバーに更新が通知されるストアの状態です。

React でストアを使用するには、createStore と useSyncExternalStore を便利な方法でラップするヘルパーである createUseStore を作成します。

export function createUseStoreT>(initialState: T) {
  const store = createStore({ initialState });
  return () => [useSyncExternalStore(store.subscribe, store.getSnapshot), store.setState] as const;
}

ストアの利用

ストアの準備ができたら、まずは Counter コンポーネントの構築から始めましょう。

import React, { useState } from "react";

export function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  const decrement = () => {
    setCount(count - 1);
  };

  return (
    div>
      p>Count: {count}p>
      button onClick={increment}>Incrementbutton>
      button onClick={decrement}>Decrementbutton>
    div>
  );
}

アプリ内で 3 回レンダリングします。

import React from 'react';
import ReactDOM from 'react-dom/client';
import { Counter } from './Counter.tsx';

ReactDOM.createRoot(document.getElementById('root')!).render(
  React.StrictMode>
    Counter />
    Counter />
    Counter />
  React.StrictMode>,
);

ページには 3 つのカウンターが表示されています。[増加]をクリックすると、カウンターの 1 つだけが増加します。

ストアを使用して、これら 3 つのカウンターが同じ状態を使用するようにします。まず、前に作成した createUseStore ヘルパーを使用して useCountStore を作成します。

export const useCountStore = createUseStore(0);

では、カウンターで useCountStore フックを使用しましょう。

import { useCountStore } from "./countStore";

function Counter() {
  const [count, setCount] = useCountStore();

  const increment = () => {
    setCount(count + 1);
  };

  const decrement = () => {
    setCount(count - 1);
  };

  return (
    div>
      p>Count: {count}p>
      button onClick={increment}>Incrementbutton>
      button onClick={decrement}>Decrementbutton>
    div>
  );
}

これで 3 つのカウンターが同期され、すべてが一緒に増加します。
3つのカウンターは同じ状態を共有するようになりました
ジェネリックの使用により、TypeScript は count が数値であり、setCount が数値を受け入れるコールバックであることを認識します。
州のTypescriptサポート
setState の Typescript サポート

次のステップ

私たちのシンプルなストアをどのように改善し、構築できるかについてのアイデアをいくつか紹介します。

状態を縮小する
ストア内で状態を設定するのは非常に直接的で便利ですが、状態を決定するときに複雑なロジックを処理する必要がある場合もあります。そこで、リデューサーが役立ちます。ストアに新しいディスパッチ関数を追加できます。

dispatch(action) {
  state = reducer(action);
  notifyStateChanged();
},

深くネストされた状態の処理
新しい状態を設定するには、既存の状態を分解する必要がありますが、深くネストされた状態がある場合は面倒な場合があります。これを解決するには、immer または同様のライブラリを使用できます。

// without immer
setState({
    ...state,
    nested: {
        ...state.nested,
        sub: {
            ...state.nested.sub,
            new: true,
        }
    }
});

// with immer
import { produce } from "immer";

const nextState = produce(state, s => {
    s.nested.sub.new = true;
});
setState(nextState);

次のように、ストアに immer を内部的に追加し、setState でコールバックを受け入れることもできます。

setState((state) => {
    state.nested.sub.new = true;
    return state;
});

結論

このチュートリアルでは、TypeScriptをサポートするシンプルなReact状態管理ライブラリを構築する手順を説明しました。Reactの useSyncExternalStore フックを使用して、React コンポーネントとシームレスに統合されるシンプルでありながら強力なストアを構築しました。これで、使い方がわかったので、独自のカスタム状態管理ライブラリを構築する準備が整いました。


詳細はこちら 同期外部ストアを使用する Reactドキュメント内。

この記事で説明した概念の実際の実装を確認するには、 タイニーステート反応 こここのライブラリは、このチュートリアルで説明されているアプローチを使用して構築されており、コードと例をさらに深く掘り下げることができます。

#40行未満のコードで独自のReact状態管理ライブラリを構築する

執筆者について: nipponese

Nipponese News編集部は、国内外のニュースを日本語で分かりやすくお届けします。