Sentry 開發者貢獻指南-前端 React Hooks 與蟲洞狀態管理模式
什么是蟲洞狀態管理模式?
您可以逃脫的最小 state 共享量是多少?
保持你的 state。盡可能靠近使用它的地方。
如果有一個組件關心這個問題,使用它。如果有幾個組件在意,就用 props 分享一下。如果很多組件都關心,把它放在 context 中。
Context 就像一個蟲洞。它使您的組件樹彎曲,因此相距很遠的部分可以接觸。
利用自定義 hooks 使這變得容易。
一個例子
構建一個點擊計數器。蟲洞狀態管理模式最好通過示例來解釋 ??
CodeSandbox(示例代碼)
- https://codesandbox.io/s/wormhole-state-pattern-5-j4w5e?file=/src/App.js
步驟 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(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>
- </>
- );
- };
我們已將 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 個按鈕更新相同的狀態怎么辦?
您可以將 count 和 setCount 作為 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 是一個更豐富的狀態,它也包含操作該狀態所需的一切。通常,這將是來自您的 reducer 的 dispatch 方法,或者像我們這里的自定義狀態設置器。
我們的 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 共享狀態,定義更簡單的 incA 和 incB 輔助方法,并返回它們的狀態。
這意味著我們的 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 獲取 count 和 incB。使用它們。
性能怎么樣?
很好。
盡可能少地共享 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(詳細代碼)
- https://github.com/getsentry/sentry/blob/master/static/app/views/organizationContext.tsx
Refs
Wormhole state management
https://swizec.com/blog/wormhole-state-management/