系列
什麼是蟲洞狀態管理模式?
您可以逃脫的最小 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>
<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
個按鈕更新相同的狀態怎麼辦?
您可以將 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(詳細程式碼)