使用 Hooks 和 Context API 替换 Redux:如何
是否可以使用新的 React Context API 和 hooks 完全取代 Redux?这样做值得吗?效果是否一样?而且这个解决方案是否和 Redux + React-redux 一样好用?
随着新的React Context API的出现,在应用程序内部传递数据变得更加容易,而有了新的hooks之后,我开始看到很多帖子宣称可以替换 Redux。我想亲自尝试一下,于是开始仔细研究 React 文档,并尝试构建自己的 Redux。
以下是我的发现和我得出的结论。
上下文 API
React 面临的挑战之一是如何将 props 传递给组件树深处的组件;这些 props 对于应用程序来说是“全局的”,许多组件可能想要使用它们,它们通常代表配置、UI 主题、翻译。
Context 提供了一种通过组件树传递数据的方法,而无需在每个级别手动传递 props。
如何使用
要开始构建类似 Redux 的库,我想为整个应用程序提供一个state
对象和一个dispatch
函数,因此让我们构建一个利用 Context API 的示例并执行此操作:
import React from "react";
// Create a context with a default value
const StateContext = React.createContext({
state: {},
dispatch: () => {}
});
const ComponentUsingContext = () => {
return (
// Wrap the component using the value with the context consumer
<StateContext.Consumer>
{({ state }) => <div>App state: {JSON.stringify(state)}</div>}
</StateContext.Consumer>
);
};
// Wrap your component with the provider and pass a value
// if you don't want to use the default
const App = () => {
return (
<StateContext.Provider
value={{
state: {
counter: 1
},
dispatch: () => console.log("dispatch")
}}
>
<ComponentUsingContext />
</StateContext.Provider>
);
};
上面快速介绍了如何使用 Context 沿组件树发送数据,它与用于包装应用程序的 React Redux Provider 看起来没有太大区别。
请注意如何首先创建一个Context
,然后使用Context.Provider
将数据发送到树中并Context.Consumer
在任何嵌套级别使用该数据。
使用的部分Context.Consumer
看起来比我想要的要复杂一些,但是有一个钩子可以让它看起来更干净(稍后会详细介绍)。
现在我们已经有办法将数据“注入”到应用程序中,让我们看看如何利用钩子来构建替代 Redux 所需的附加功能。
钩子
React 16.8.0 中引入了 Hooks 来解决不同类型的问题:
- 使组件之间的状态逻辑重用变得更容易
- 远离类,它们固有的冗长和使用这个
- 更多地利用提前编译来创建优化代码(并且类可以鼓励使其变得困难的模式)
- 可能还有其他原因,我不知道😇
在 React 自带的所有钩子中,useContext
和useReducer
可以帮助在 React 中构建类似 Redux 的库。
useContext
const value = useContext(MyContext);
它是使用模式的替代方法Context.Consumer
(并且在我看来使代码看起来更具可读性)。
让我们看看它如何应用于前面的 Context 示例:
import React, { useContext } from "react";
const StateContext = React.createContext({
state: {},
dispatch: () => {}
});
const ComponentUsingContext = () => {
const { state } = useContext(StateContext); // <---
return <div>App state: {JSON.stringify(state)}</div>;
};
const App = () => {
return (
<StateContext.Provider
value={{
state: {
counter: 1
},
dispatch: () => console.log("dispatch")
}}
>
<ComponentUsingContext />
</StateContext.Provider>
);
};
您仍然需要使用Context.Provider
,但是从上下文中检索值现在看起来好多了。
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
该useReducer
钩子接受一个 reducer(与您为 Redux 编写的相同)和一个初始状态,并使用调度方法返回新状态。
state
并且dispatch
正是我需要通过该应用程序传递的内容React.Context
。
尝试把事情整合起来
我的类似 Redux 的库的 API 应该包括:
- 包装
Provider
应用程序并注入状态和调度方法 - 创建存储(包含状态和调度方法)的方法
useStore
,传递给提供者 connect
将组件挂接到状态的方法
提供者
提供者仅仅是Context.Provider
:
const Context = React.createContext(); // No default needed here
export const Provider = Context.Provider;
连接
一个非常基本的方法connect
是接受一个Component
,然后利用来useContext
获取state
和dispatch
,然后将它们传递给它。
export const connect = Component = () => {
const { state, dispatch } = useContext(Context);
const props = { state, dispatch };
return React.createElement(Component, props, null);
};
这当然是一个非常基本的版本,它将整个状态传递给组件:这并不是我想要的。
介绍mapStateToProps
和mapDispatchToProps
Reduxconnect
方法利用mapStateToProps
将整个状态映射到组件所需的 props。
它还用于mapDispatchToProps
将调度方法包装的操作作为道具传递给组件。
我也想支持这些方法,所以这是一个改进的版本,它还支持组件自己的道具:
export const connect = (
mapStateToProps = () => ({}),
mapDispatchToProps = () => ({})
) => Component => ownProps => {
const { getState, dispatch } = useContext(Context);
const stateProps = mapStateToProps(getState(), ownProps);
const dispatchProps = mapDispatchToProps(dispatch, ownProps);
const props = { ...ownProps, ...stateProps, ...dispatchProps, dispatch };
return createElement(Component, props, null);
};
mapStateToProps
因此,我在这里添加了对和 的支持mapDispatchToProps
,并提供了一个默认值,如果未提供这些参数,则返回一个空对象。然后,我添加了 ,dispatch
以便组件可以使用它来调度操作。
useStore
这只是一个实用钩子,用于useReducer
创建并返回一个 store,类似于createStore
Redux。它还创建了一个getState
返回 state 的函数。
export const useStore = (reducer, initialState = {}) => {
const [state, dispatch] = useReducer(reducer, initialState);
const getState = () => state;
return { getState, dispatch };
};
以下代码片段将所有内容放在同一个文件中,以使其更易于阅读和理解:
import { createElement, createContext, useReducer, useContext } from "react"; | |
const Context = createContext(); | |
export const ContextProvider = Context.Provider; | |
export const useStore = (reducer, initialState = {}) => { | |
const [state, dispatch] = useReducer(reducer, initialState); | |
const getState = () => state; | |
return { getState, dispatch }; | |
}; | |
export const connect = ( | |
mapStateToProps = () => ({}), | |
mapDispatchToProps = () => ({}) | |
) => Component => ownProps => { | |
const { getState, dispatch } = useContext(Context); | |
const stateProps = mapStateToProps(getState(), ownProps); | |
const dispatchProps = mapDispatchToProps(dispatch, ownProps); | |
const props = { ...ownProps, ...stateProps, ...dispatchProps, dispatch }; | |
return createElement(Component, props, null); | |
}; |
import { createElement, createContext, useReducer, useContext } from "react"; | |
const Context = createContext(); | |
export const ContextProvider = Context.Provider; | |
export const useStore = (reducer, initialState = {}) => { | |
const [state, dispatch] = useReducer(reducer, initialState); | |
const getState = () => state; | |
return { getState, dispatch }; | |
}; | |
export const connect = ( | |
mapStateToProps = () => ({}), | |
mapDispatchToProps = () => ({}) | |
) => Component => ownProps => { | |
const { getState, dispatch } = useContext(Context); | |
const stateProps = mapStateToProps(getState(), ownProps); | |
const dispatchProps = mapDispatchToProps(dispatch, ownProps); | |
const props = { ...ownProps, ...stateProps, ...dispatchProps, dispatch }; | |
return createElement(Component, props, null); | |
}; |
一个工作示例
以下是使用我刚刚讨论的代码的常见反例(注意我的 CSS 技能):
关于重新渲染的重要说明
您可能想知道应用程序如何重新渲染,因为我从未使用过setState
,这是触发 React 重新渲染的要求。
forceUpdate
在Redux中,当store发生变化时会触发connect方法,但是在这里呢?
解决方案在于useContext
钩子的工作方式:
useContext
当上下文值改变时,组件调用将始终重新渲染。
有关更多信息,请参阅React 文档。
现在去哪儿?
当然这个例子远没有Redux强大,但是证明了Redux可以被Context+Hooks替代。
但这是对的吗?把这些新的 React 特性打包成一个类似 Redux 的库,是正确的模式吗?
我相信这些新工具为我们提供了寻找新模式的机会,并利用钩子提供的可重用性来找到在任何嵌套级别共享和访问应用程序状态的更好方法。
我们将本着真正的敏捷精神,通过一次又一次的迭代找到“正确的方法”。
本文最初发表于onefiniteloop.io。
鏂囩珷鏉ユ簮锛�https://dev.to/ricca509/replace-redux-with-hooks-and-the-context-api-how-to-4m77