Sentry 開發者貢獻指南 - 前端 React Hooks 與蟲洞狀態管理模式

為少發表於2021-12-18

系列

什麼是蟲洞狀態管理模式?

您可以逃脫的最小 state 共享量是多少?

保持你的 state。儘可能靠近使用它的地方。

如果有一個元件關心這個問題,使用它。如果有幾個元件在意,就用 props 分享一下。 如果很多元件都關心,把它放在 context 中。

Context 就像一個蟲洞。它使您的元件樹彎曲,因此相距很遠的部分可以接觸。

利用自定義 hooks 使這變得容易。

一個例子

構建一個點選計數器。蟲洞狀態管理模式最好通過示例來解釋 ✌️

CodeSandbox(示例程式碼)

步驟 1

我們從 useState 開始,因為它是最簡單的。

const ClickCounter = () => {
  const [count, setCount] = useState(0);

  function onClick() {
    setCount(count => count + 1);
  }

  return <button onClick={onClick}>{count} +1</button>;
};

count 儲存當前的點選次數,setCount 讓我們在每次點選時更新值。

足夠簡單。

不過,外觀並不是很漂亮。讓我們用一個自定義按鈕元件和一些巢狀來改進它。

步驟 2

我們建立了一個可重複使用的 PrettyButton,確保您應用中的每個按鈕看起來都很棒。

狀態保留在 ClickCounter 元件中。

const ClickCounter = () => {
  const [count, setCount] = useState(0);

  function onClick() {
    setCount(count => count + 1);
  }

  return (
    <>
      <p>You have clicked buttons {count} times</p>
      <div style={{ textAlign: "right" }}>
        <PrettyButton onClick={onClick}>+1</PrettyButton>
      </div>
    </>
  );
};

這是必要的最少狀態共享。我們也保持了簡單的狀態。

計數器元件關心點選次數計數,因此它將回撥作為 props 傳遞到按鈕中。函式被呼叫,狀態更新,元件重新渲染。

不需要複雜的操作。

步驟 3

如果我們的狀態更復雜怎麼辦? 我們有 2 個屬於一起的項。

您可以在您的狀態中保留複雜的值。效果很好。

const ClickCounter = () => {
  const [count, setCount] = useState({ A: 0, B: 0 });

  function onClickA() {
    setCount(count => {
      return { ...count, A: count.A + 1 };
    });
  }

  function onClickB() {
    setCount(count => {
      return { ...count, B: count.B + 1 };
    });
  }

  return (
    <>
      <p>
        You have clicked buttons A: {count.A}, B: {count.B} times
      </p>
      <div style={{ textAlign: "right" }}>
        <PrettyButton onClick={onClickA}>A +1</PrettyButton>
        &nbsp;
        <PrettyButton onClick={onClickB}>B +1</PrettyButton>
      </div>
    </>
  );
};

我們已將 count 拆分為一個物件 – { A, B }

現在單個狀態可以儲存多個值。單獨按鈕點選的單獨計數。

React 使用 JavaScript 相等來檢測重新渲染的更改,因此您必須在每次更新時製作完整狀態的副本。這在大約 10,000 個元素時變慢。

您也可以在這裡使用 useReducer
特別是當您的狀態變得更加複雜並且專案經常單獨更新時。

使用 useReducer 的類似狀態如下所示:

const [state, dispatch] = useReducer((action, state) => {
	switch (action.type) {
		case 'A':
			return { ...state, A: state.A + 1 }
		case 'B':
			return { ...state, A: state.A + 1 }
	}
}, { A: 0, B: 0})

function onClickA() {
	dispatch({ type: 'A' })
}

你的狀態越複雜,這就越有意義。

但我認為那些 switch 語句很快就會變得混亂,而且你的回撥函式無論如何都已經是動作了。

步驟 4

如果我們想要 2 個按鈕更新相同的狀態怎麼辦?

您可以將 countsetCount 作為 props 傳遞給您的元件。但這變得越來越混亂。

const AlternativeClick = ({ count, setCount }) => {
  function onClick() {
    setCount(count => {
      return { ...count, B: count.B + 1 };
    });
  }

  return (
    <div style={{ textAlign: "left" }}>
      You can also update B here
      <br />
      <PrettyButton onClick={onClick}>B +1</PrettyButton>
      <p>It's {count.B} btw</p>
    </div>
  );
};

我們建立了一個難以移動並且需要理解太多父邏輯的元件。關注點是分裂的,抽象是奇怪的,我們造成了混亂。

你可以通過只傳遞它需要的狀態部分和一個更自定義的 setCount 來修復它。但這是很多工作。

步驟 5

相反,您可以使用蟲洞與自定義 hook 共享狀態。?

您現在有 2 個共享狀態的獨立元件。將它們放在您的程式碼庫中的任何位置,它 Just Works™

需要在其他地方訪問共享狀態?新增 useSharedCount hook,瞧。

這是這部分的工作原理。

我們有一個 context provider,裡面有一些操作:

export const SharedCountProvider = ({ children }) => {
  // replace with useReducer for more flexiblity
  const [state, setState] = useState(defaultState);

  const [contextValue, setContextValue] = useState({
    state,
    // dispatch // from your reducer
    // this is where a reducer comes handy when this grows
    setSharedCount: (key, val) => {
      setState(state => {
        return { ...state, [key]: val };
      });
    }
    // other stuff you need in context
  });

  // avoids deep re-renders
  // when instances of stuff in context change
  useEffect(() => {
    setContextValue(currentValue => ({
      ...currentValue,
      state
    }));
  }, [state]);

  return (
    <SharedCountContext.Provider value={contextValue}>
      {children}
    </SharedCountContext.Provider>
  );
};

Context Provider 使用豐富的 state 變數來保持您的狀態。
這裡對我們來說是 { A, B }

contextValue 是一個更豐富的狀態,它也包含操作該狀態所需的一切。通常,這將是來自您的 reducerdispatch 方法,或者像我們這裡的自定義狀態設定器。

我們的 setSharedCount 方法獲取一個 key 和一個 val 並更新該部分狀態。

setSharedCount("B", 10);

然後我們有一個副作用,它觀察 state 的變化並在需要時觸發重新渲染。這避免了每次我們重新定義我們的 dispatch 方法或其他任何東西時的深度重新渲染。

使 React 樹更穩定 ✌️

在這個 provider 中呈現的每個元件都可以使用這個相同的自定義 hook 來訪問它需要的一切。

export function useSharedCount() {
  const { state, setSharedCount } = useContext(SharedCountContext);

  function incA() {
    setSharedCount("A", state.A + 1);
  }

  function incB() {
    setSharedCount("B", state.B + 1);
  }

  return { count: state, incA, incB };
}

自定義 hook 利用 React Context 共享狀態,定義更簡單的 incAincB 輔助方法,並返回它們的狀態。

這意味著我們的 AlternativeClick 元件可以是這樣的:

import {
  useSharedCount
} from "./SharedCountContextProvider";

const AlternativeClick = () => {
  const { count, incB } = useSharedCount();

  return (
    <div style={{ textAlign: "left" }}>
      You can also update B here
      <br />
      <PrettyButton onClick={incB}>B +1</PrettyButton>
      <p>It's {count.B} btw</p>
    </div>
  );
};

從自定義 hook 獲取 countincB。使用它們。

效能怎麼樣?

很好。

儘可能少地共享 state。對應用程式的不同部分使用不同的 context provider

不要讓它成為 global,除非它需要是 global 的。包裹你可以逃脫的樹的最小部分。

複雜度如何?

什麼複雜度?保持小。不要把你不需要的東西塞進去。

討厭管理自己的狀態

看到我們 SharedCountProvider 中處理狀態變化的部分了嗎? 這部分:

const [contextValue, setContextValue] = useState({
    state,
    // dispatch // from your reducer
    // this is where a reducer comes handy when this grows
    setSharedCount: (key, val) => {
      setState(state => {
        return { ...state, [key]: val };
      });
    }
    // other stuff you need in context
  });

為此,您可以使用 XState。或者 reducer。甚至 Redux,如果你真的想要的話。

不過,如果你使用 Redux,你不妨一路走下去 ?

頂級開源專案是如何使用的?(Sentry)

organizationContext.tsx(詳細程式碼)

Refs

相關文章