系列
什么是蟲洞狀態管理模式?
您可以逃脫的最小 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(詳細代碼)