我说过,React 不需要状态管理工具
时不时还是会有人告诉我,他们在项目中使用了 REDUX 或类似的工具。我通常会回答说,我不会用它们,因为现在有了 hooks 和 context API,就不需要它们了。
但是上下文 API 通常会带来性能问题,并且正确使用起来也有点尴尬,所以今天我将尝试展示如何避免常见问题,以及如何构建自己的(微)状态管理工具,而不会做出任何妥协。
简单的解决方案
基本思想是在一个组件中管理状态并通过上下文传递整个状态,以便所有子组件都可以访问它,这样我们就可以避免 props 钻孔。
export const StateContext = createContext(null);
const Provider = () => {
return (
<StateContext.Provider value={state}>
<ChildComponent />
</StateContext.Provider>
)
}
使用 dispatch
但是,您还需要某种方法来修改子组件的状态。您可以将单独的函数传递给上下文,但我个人不喜欢这样做,因为状态会很快变得复杂。我喜欢调度事件的想法(类似于 REDUX),所以我们基本上传递一个函数,您可以使用它来调度所需的所有不同操作。我们可以将它与状态传递到同一个上下文中,但我不喜欢将它与状态混合,所以我将它传递到一个单独的上下文中。
const StateContext = createContext(null);
const DispatchContext = createContext(null);
export const Provider = () => {
const [state, setState] = useState(...)
const dispatch = (action) => {
switch (action.type) {
case 'CHANGE_STATE':
setState(action.payload)
break;
...
}
}
return (
<StateContext.Provider value={{state, ...}}>
<DispatchContext.Provider value={dispatch}>
<ChildComponent />
</DispatchContext.Provider>
</StateContext.Provider>
)
}
我还喜欢创建钩子来获取调度函数,使其更加明确:
export const useDispatch = () => {
return useContext(DispatchContext)
}
基本上,我们将数据与操作分离——提供组件向子组件提供数据。子组件可以调度操作来修改数据,但数据由提供组件控制,因此它拥有对数据的控制权。调度的操作可以理解为类似于 DOM 事件,只是我们知道谁会接收它。
现在让我们看看性能方面,如果我们想用它来替代 REDUX,它需要能够处理大量订阅组件的大状态。
避免不必要的儿童再创造
这种配置效率极低,因为每次我们修改状态时,所有子组件都会重新渲染。这是因为每次我们更新 Provider 组件的状态时,它的所有子组件都会被重新创建。我们可以在子组件上使用 React.memo 来避免这种情况,但更好的解决方案是从上级组件传递子组件,这样当 Provider 更新时,子组件将保持不变。而且我们只更新上下文的实际使用者。
export const Provider = ({ children }) => {
...
return (
<StateContext.Provider value={{state, ...}}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
)
}
在父级中我们这样做:
export const Parent = ({ children }) => {
return (
<Provider>
<ChildComponent />
</Provider>
)
}
现在,提供程序组件正在管理上下文,但不再管理子组件(仅传递它们)。我花了一段时间才理解这个细微的差别,因为代码中的变化很小,却带来了很大的影响。
诀窍在于理解,当我们放置时<ChildComponent >
,我们基本上每次都在创建新的 React.Node,因此所有子项都会重新渲染,除非我们将它们包装在内React.memo
。
因此,通过这一改变,我们只更新使用上下文的组件。
避免调度导致的重新渲染
目前,每次状态更改时,dispatch 函数都会重新创建,这意味着所有使用它的组件都将被重新渲染,即使它们没有使用 StateContext。通常,如果我们想要拥有稳定的函数,React 文档建议使用useCallback
,但在这种情况下,它只能部分帮助我们,因为这基本上会导致 dispatch 函数的“缓存”,并且我们将无法在不包含外部作用域变量的情况下使用它们dependencies
- 而且,当依赖项更改时,dispatch 函数仍然会被重新创建。我们需要使用ref
来帮助我们解决这个问题。
...
export const Provider = ({ children }) => {
const [state, setState] = useState(...)
const dispatchRef = useRef()
// new function with every render
const dispatchRef.current = (action) => {
switch (action.type) {
case 'CHANGE_STATE':
// we can use outer scope without restrictions
setState({...action.payload, ...state})
break;
...
}
}
// stable dispatch function
const dispatch = useCallback(
(action: ActionType) => dispatchRef.current(action),
[dispatchRef]
);
return (
<StateContext.Provider value={{state, ...}}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
)
}
这样,稳定的调度函数就被传递给了DispatchContext
,我们可以不受限制地使用外部范围。
可订阅上下文
我们需要的最后一个优化是让组件能够仅订阅部分状态。现在,组件只能使用整个状态,即使它们只需要一小部分(例如一个布尔值),每次我们更改状态时,它们都会收到通知。这不是最佳实践,因为我们仍然会进行不必要的重新渲染。解决这个问题的方法是通过use-context-selector。
这个库非常简单,它允许使用选择器功能从状态中“挑选”我们想要的东西。
import { createContext } from 'use-context-selector';
const StateContext = createContext(null);
export const Provider = ({ children }) => {
return (
<StateContext.Provider value={{state, ...}}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
)
}
import { useContextSelector } from 'use-context-selector';
export const Subscriber = () => {
const somePart = useContextSelector(StateContext, context => context.somePart)
}
哦,等等,这是作弊!你不是说了你只会用 Context API 吗?
这个库是 React.Context API 的简单封装。它用于ref
封装传递的值,这样组件就不会自动重新渲染,并保存订阅者列表。当值发生变化时,它会运行所有订阅的函数;如果来自选择器的值与之前不同,它会强制订阅的组件重新渲染。类似的概念也用在了 Redux 的useSelector hook 中。所以,我想说,这是一个相当标准的解决方案,既然已经有了,为什么还要重新构建一个呢?
PS:甚至有一个开放的 RFC可以将类似这样的内容直接添加到 React 中
最终产品
我们可以包装整个功能以实现可重复使用(+添加 TypeScript 类型)
import React, { useCallback, useRef } from 'react';
import { createContext, useContextSelector } from 'use-context-selector';
type DispatchType<ActionType, DispatchReturn> = (
action: ActionType
) => DispatchReturn;
type SelectorType<StateType> = (state: StateType) => any;
export const createProvider = <
StateType,
ActionType,
DispatchReturn,
ProviderProps
>(
body: (
props: ProviderProps
) => [state: StateType, dispatch: DispatchType<ActionType, DispatchReturn>]
) => {
const StateContext = createContext<StateType>(null as any);
const DispatchContext = React.createContext<
DispatchType<ActionType, DispatchReturn>
>(null as any);
const Provider: React.FC<ProviderProps> = ({ children, ...props }) => {
const [state, _dispatch] = body(props as any);
const dispatchRef = useRef(_dispatch);
dispatchRef.current = _dispatch;
// stable dispatch function
const dispatch = useCallback(
(action: ActionType) => dispatchRef.current?.(action),
[dispatchRef]
);
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
};
const useDispatch = () => React.useContext(DispatchContext);
const useStateContext = (selector: SelectorType<StateType>) =>
useContextSelector(StateContext, selector);
return [Provider, useDispatch, useStateContext] as const;
};
使用示例
type ActionType =
| { type: 'CHANGE_STATE'; payload: ... }
...
export const [
TranslationsContextProvider,
useTranslationsDispatch,
useTranslationsSelector,
] = createProvider(
(props /* provider props */) => {
const [state1, setState1] = useState(...)
const [state2, setState2] = useState(...)
const {data, isLoading} = useQuery(...)
const dispatch = (action: ActionType) => {
switch (action.type) {
case 'CHANGE_STATE':
setState(action.payload)
break;
...
}
}
const state = {
state1,
state2,
data,
isLoading
}
// don't forget to return state and dispatch function
return [state, dispatch]
})
让我们总结一下这个解决方案的优点:
- 使用简单,不需要学习任何新内容,也不需要像 REDUX 等那样使用样板。
- 比单纯使用 Context api 更高效
- 当你拥有 hooks 的全部功能时,它会扩展
- 您可以使用多个实例,并将它们限定在需要它们的应用程序部分
在 Tolgee.io 中,我们在最复杂的视图上使用它,我们在其中处理翻译表,并且我们还没有遇到任何问题。
你怎么认为?
文章来源:https://dev.to/tolgee_i18n/react-doesnt-need-state-management-tool-i-said-31l4附言:查看Tolgee.io并给我们github stars