阿川私房教材:學程式,拿 offer!

63 個專案實戰,直接上手!
無需補習,按步驟打造你的面試作品。

立即解鎖你的轉職秘笈

你有沒有想過 React 狀態管理函式庫是如何建構的?從像 redux 這樣具有大量樣板和大包大小的解決方案,到像 zustand 或 jotai 這樣更輕、更簡單的庫。今天我們將建立我們自己的狀態管理庫,並看看幕後發生的魔法。

了解 useSyncExternalStore

React 18 引入了一個名為useSyncExternalStore的新鉤子,它允許 React 同步到任何外部儲存。

useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)

以下是其參數的詳細說明:

  • subscribe接收一個回呼作為參數,並將該回調訂閱到外部 store,以便當 store 狀態改變時呼叫它,需要傳回一個取消訂閱函數。

  • getSnapshot取得儲存的目前快照,該快照必須是快取值,因為 React 使用Object.is(getSnapshot(), oldSnapshot)在每次渲染上比較該值,每次提供新值將導致無限循環。

  • getServerSnapshot (可選)允許我們在伺服器上渲染時返回快照,這在外部儲存或訂閱源無法在伺服器上執行或需要特定處理才能在伺服器上執行的某些情況下很有幫助。

利用 useSyncExternalStore,我們可以根據我們的要求建立一個簡約的 store 。

為什麼不直接使用 React Context?

React Context是 React 中的一個功能,它允許元件將 props 傳遞到其下面的整個元件樹,這意味著它可以用作存儲,是一個可行的選擇。

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>
  );
}

大量使用 Context 可能會導致“Context Hell”,其中大量上下文提供者嵌套在 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 將導致整個應用程式的重新渲染(外部和其他都會重新渲染),因為狀態是在應用程式元件上設定的,並且當它重新渲染時,它的所有子元件元件也被重新渲染。

此外,使用外部存儲可以讓我們更輕鬆地與 http 請求等外部系統同步反應,在上下文中您將使用 useEffect,而使用外部存儲您可以直接更新存儲,更改將在訂閱元件。

建造我們的 store

讓我們深入研究一下我們 store 的實現。我們將從一個基本結構開始,然後根據我們的要求逐步增強它。

import { useSyncExternalStore } from 'react';

export type Listener = () => void;

function createStore<T>({ 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();
    },
  };
}

訂閱者是一組偵聽器,我們的 store 將在 store 狀態的每次變更時通知它們。

State是 store 的狀態,我們將在呼叫 setState 時更新它,然後通知所有 store 的訂閱者更新。

為了在 React 中使用 store,我們將建立 createUseStore,它是一個以方便的方式包裝 createStore 和 useSyncExternalStore 的幫助器:

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

使用 store

store 就位後,讓我們開始建立一個 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}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

並在我們的應用程式中渲染三次:

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>,
);

現在,我們在頁面中看到三個計數器,點擊「增量」只會增量其中一個計數器:

計數器起始點

讓我們使用我們的 store 讓這 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}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

現在我們的 3 個計數器已同步,並且所有計數器一起遞增:

3 個計數器現在共用相同的狀態

由於使用了泛型,TypeScript 知道 count 是一個數字,而 setCount 是一個接受數字的回呼:

Typescript 對狀態的支持

Typescript 對 setState 的支持

下一步

關於如何改進和建立我們的簡單 store 的一些想法:

還原狀態

在我們的儲存中設定狀態非常直接,這很方便,但有時我們在確定狀態時可能需要處理複雜的邏輯,這就是減速器可能幫助我們的地方,我們可以為我們的儲存加入一個新的調度函數:

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 加入到我們的 store,並在 setState 中接受回調,如下所示:

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

結論

在本教學中,我們完成了建立一個具有 TypeScript 支援的簡單 React 狀態管理函式庫的步驟。

透過利用 React 的useSyncExternalStore鉤子,我們建立了一個簡單但功能強大的存儲,可以與 React 元件無縫整合。

現在您已經掌握了它的竅門,您就可以建立自己的自訂狀態管理庫了。


在 React 文件中閱讀有關useSyncExternalStore 的更多資訊。

要查看本文中討論的概念的實際實現,請查看此處的tinystate-react 。該庫是使用本教程中描述的方法建置的,可讓您更深入地研究程式碼和範例。


原文出處:https://dev.to/paripsky/build-your-own-react-state-management-library-in-under-40-lines-of-code-with-typescript-support-hji


共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。

阿川私房教材:學程式,拿 offer!

63 個專案實戰,直接上手!
無需補習,按步驟打造你的面試作品。

立即解鎖你的轉職秘笈