Redux 与 React Context API 的比较
React 16.3 添加了一个新的 Context API——之所以说是新的,是因为旧的context API 是一个幕后功能,大多数人要么不知道,要么因为文档建议避免使用它而避免使用。
不过,现在 Context API 已成为 React 中的一等公民,对所有人开放(虽然以前也开放,但现在算是正式的了)。
React 16.3 发布后,网络上立刻涌现出大量文章,宣称 Redux 因为新的 Context API 而走向终结。但如果你问 Redux,我想它会说“关于我已死的传言大大夸大了”。
在这篇文章中,我想介绍新的 Context API 的工作原理,它与 Redux 的相似之处,何时应该使用 Context而不是Redux,以及为什么 Context 不能在所有情况下都取代 Redux。
一个激励人心的例子
我假设你已经掌握了 React 的基础知识(props 和 state),但如果你还没有,我这里有一个免费的 5 天课程可以帮助你学习 React。
让我们来看一个大多数人都会选择使用 Redux 的例子。我们先从一个纯 React 版本开始,然后看看它在 Redux 中的样子,最后看看它在 Context 中的样子。
该应用将用户信息显示在两个位置:右上角的导航栏和主要内容旁边的侧边栏。
组件结构如下所示:
使用纯 React(仅使用常规 props),我们需要将用户信息存储在足够高的层级,以便将其传递给需要它的组件。在这种情况下,用户信息的保管者必须是App……
然后,为了将用户信息传递给需要它的组件,App 需要将其传递给 Nav 和 Body。Nav 和 Body 又需要将其再次传递给 UserAvatar(太棒了!)和 Sidebar。最后,Sidebar 必须将其传递给 UserStats。
让我们看看这在代码中是如何实现的(为了便于阅读,我把所有内容都放在一个文件中,但实际上这些内容可能会被拆分成单独的文件)。
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const UserAvatar = ({ user, size }) => (
<img
className={`user-avatar ${size || ""}`}
alt="user avatar"
src={user.avatar}
/>
);
const UserStats = ({ user }) => (
<div className="user-stats">
<div>
<UserAvatar user={user} />
{user.name}
</div>
<div className="stats">
<div>{user.followers} Followers</div>
<div>Following {user.following}</div>
</div>
</div>
);
const Nav = ({ user }) => (
<div className="nav">
<UserAvatar user={user} size="small" />
</div>
);
const Content = () => <div className="content">main content here</div>;
const Sidebar = ({ user }) => (
<div className="sidebar">
<UserStats user={user} />
</div>
);
const Body = ({ user }) => (
<div className="body">
<Sidebar user={user} />
<Content user={user} />
</div>
);
class App extends React.Component {
state = {
user: {
avatar:
"https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
name: "Dave",
followers: 1234,
following: 123
}
};
render() {
const { user } = this.state;
return (
<div className="app">
<Nav user={user} />
<Body user={user} />
</div>
);
}
}
ReactDOM.render(<App />, document.querySelector("#root"));
这倒也不算太糟糕,运行起来也没什么问题。但写起来有点麻烦。而且当你需要传递很多属性(而不是只有一个)时,就更麻烦了。
然而,这种“属性传递”策略也存在一个更大的弊端:它会在原本应该解耦的组件之间建立耦合。在上面的例子中,组件Nav需要接收一个“user”属性并将其传递给组件UserAvatar,即使导航组件本身并不需要这个user属性。
紧密耦合的组件(例如将属性传递给子组件的组件)更难重用,因为每当将一个组件放置在新的位置时,都必须将其与新的父组件连接起来。
让我们来看看如何用 Redux 来改进它。
使用 Redux 改进数据流
我将快速浏览一下 Redux 示例,以便我们能够更深入地了解 Context 的工作原理,所以如果您对 Redux 还不熟悉,请先阅读这篇Redux 简介(或观看视频)。
这是上面提到的 React 应用,已经重构为使用 Redux。user信息已移至 Redux store,这意味着我们可以使用 react-redux 的connect函数直接将userprop 注入到需要它的组件中。
就解耦而言,这是一项重大胜利。看看 `<function>` Nav、Body`<function>` 和Sidebar`<function>`,你会发现它们不再接收和传递userprop。不再有 prop 的传递,也不再有不必要的耦合。
import React from "react";
import ReactDOM from "react-dom";
// We need createStore, connect, and Provider:
import { createStore } from "redux";
import { connect, Provider } from "react-redux";
// Create a reducer with an empty initial state
const initialState = {};
function reducer(state = initialState, action) {
switch (action.type) {
// Respond to the SET_USER action and update
// the state accordingly
case "SET_USER":
return {
...state,
user: action.user
};
default:
return state;
}
}
// Create the store with the reducer
const store = createStore(reducer);
// Dispatch an action to set the user
// (since initial state is empty)
store.dispatch({
type: "SET_USER",
user: {
avatar: "https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
name: "Dave",
followers: 1234,
following: 123
}
});
// This mapStateToProps function extracts a single
// key from state (user) and passes it as the `user` prop
const mapStateToProps = state => ({
user: state.user
});
// connect() UserAvatar so it receives the `user` directly,
// without having to receive it from a component above
// could also split this up into 2 variables:
// const UserAvatarAtom = ({ user, size }) => ( ... )
// const UserAvatar = connect(mapStateToProps)(UserAvatarAtom);
const UserAvatar = connect(mapStateToProps)(({ user, size }) => (
<img
className={`user-avatar ${size || ""}`}
alt="user avatar"
src={user.avatar}
/>
));
// connect() UserStats so it receives the `user` directly,
// without having to receive it from a component above
// (both use the same mapStateToProps function)
const UserStats = connect(mapStateToProps)(({ user }) => (
<div className="user-stats">
<div>
<UserAvatar user={user} />
{user.name}
</div>
<div className="stats">
<div>{user.followers} Followers</div>
<div>Following {user.following}</div>
</div>
</div>
));
// Nav doesn't need to know about `user` anymore
const Nav = () => (
<div className="nav">
<UserAvatar size="small" />
</div>
);
const Content = () => (
<div className="content">main content here</div>
);
// Sidebar doesn't need to know about `user` anymore
const Sidebar = () => (
<div className="sidebar">
<UserStats />
</div>
);
// Body doesn't need to know about `user` anymore
const Body = () => (
<div className="body">
<Sidebar />
<Content />
</div>
);
// App doesn't hold state anymore, so it can be
// a stateless function
const App = () => (
<div className="app">
<Nav />
<Body />
</div>
);
// Wrap the whole app in Provider so that connect()
// has access to the store
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector("#root")
);
你可能想知道 Redux 是如何实现这种神奇功能的。这确实是个值得思考的问题。为什么 React 不支持多层传递 props,而 Redux 却能做到呢?
答案是,Redux 使用的是 React 的Context特性。但不是最新的 Context API(目前还不是),而是旧版本。React 文档中明确指出,除非你在编写库或者非常清楚自己在做什么,否则不要使用旧版本。
上下文就像一条运行在每个组件背后的电力总线:要接收流经它的电力(数据),你只需要插入即可。而(React-)Redux 的connect函数正是如此。
不过,Redux 的这个特性只是冰山一角。数据传递只是 Redux 众多特性中最显而易见的一个。以下是 Redux 的其他一些开箱即用的优势:
connect纯
connect它会自动将连接的组件设为“纯组件”,这意味着它们只会在 props 发生变化时重新渲染——也就是当它们在 Redux 状态中的切片发生变化时。这可以防止不必要的重新渲染,从而保持应用程序的快速运行。DIY 方法:创建一个继承自 `PropertyComponent` 的类PureComponent,或者shouldComponentUpdate自己实现 `PropertyComponent`。
使用 Redux 轻松调试
编写 action 和 reducer 的过程虽然繁琐,但它却能为你提供强大的调试能力。
使用Redux DevTools 扩展,您可以自动记录应用程序执行的每个操作。您可以随时打开日志,查看触发了哪些操作、它们的有效负载以及操作发生前后的状态。
Redux DevTools 的另一个强大功能是时间旅行调试,也就是说,你可以点击任何过去的某个操作,跳转到那个时间点,基本上可以重放直到该操作为止的所有操作(但不能更早)。之所以能实现这一点,是因为每个操作都会不可变地更新状态,所以你可以获取已记录的状态更新列表并进行重放,而不会产生任何不良影响,最终得到你想要的结果。
此外,还有像LogRocket这样的工具,它基本上可以为每个用户提供一个始终在线的生产环境Redux DevTools 。收到 bug 报告?太好了。在 LogRocket 中查找该用户的会话,即可查看他们的操作回放,以及具体触发了哪些 action。这一切都是通过访问 Redux 的 action 流来实现的。
使用中间件自定义 Redux
Redux 支持中间件的概念,中间件其实就是“每次分发 action 时都会运行的函数”。编写自己的中间件并没有想象中那么难,而且它能实现一些强大的功能。
例如…
- 想在每次操作名称以特定前缀开头时都发起一个 API 请求吗
FETCH_?你可以使用中间件来实现。 - 想要一个集中化的地方来记录分析软件中的事件吗?中间件是实现这一目标的理想选择。
- 想要阻止某些操作在特定时间执行?你可以使用中间件来实现,这对你的应用程序的其他部分是透明的。
- 想要拦截带有 JWT 令牌的操作并将其自动保存到 localStorage 中吗?没错,中间件就派上用场了。
这里有一篇不错的文章,其中包含一些关于如何编写 Redux 中间件的示例。
如何使用 React Context API
但是,也许你并不需要 Redux 的所有那些花哨功能。也许你不在乎便捷的调试、自定义或自动性能优化——你只想轻松地传递数据。也许你的应用很小,或者你只需要先让应用运行起来,以后再考虑那些花哨的功能。
React 的新 Context API 或许能满足需求。让我们看看它是如何工作的。
如果你更喜欢观看而不是阅读,我在 Egghead 上发布了一个关于 Context API 的简短教程(3:43):
上下文 API 由 3 个重要部分组成:
React.createContext创建上下文的函数Provider返回的值,createContext用于建立贯穿组件树的“电气总线”。- (
Consumer也由返回createContext)它接入“电力总线”以提取数据
它Provider与 React-Redux 的非常相似Provider。它接受一个valueprop,可以是任何你想要的内容(甚至可以是 Redux store……但这有点傻)。它很可能是一个包含你的数据以及你想对数据执行的任何操作的对象。
它Consumer的工作原理有点像 React-Redux 的connect函数,即获取数据并将其提供给使用该数据的组件。
以下是重点内容:
// Up top, we create a new context
// This is an object with 2 properties: { Provider, Consumer }
// Note that it's named with UpperCase, not camelCase
// This is important because we'll use it as a component later
// and Component Names must start with a Capital Letter
const UserContext = React.createContext();
// Components that need the data tap into the context
// by using its Consumer property. Consumer uses the
// "render props" pattern.
const UserAvatar = ({ size }) => (
<UserContext.Consumer>
{user => (
<img
className={`user-avatar ${size || ""}`}
alt="user avatar"
src={user.avatar}
/>
)}
</UserContext.Consumer>
);
// Notice that we don't need the 'user' prop any more,
// because the Consumer fetches it from context
const UserStats = () => (
<UserContext.Consumer>
{user => (
<div className="user-stats">
<div>
<UserAvatar user={user} />
{user.name}
</div>
<div className="stats">
<div>{user.followers} Followers</div>
<div>Following {user.following}</div>
</div>
</div>
)}
</UserContext.Consumer>
);
// ... all those other components go here ...
// ... (the ones that no longer need to know or care about `user`)
// At the bottom, inside App, we pass the context down
// through the tree using the Provider
class App extends React.Component {
state = {
user: {
avatar:
"https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
name: "Dave",
followers: 1234,
following: 123
}
};
render() {
return (
<div className="app">
<UserContext.Provider value={this.state.user}>
<Nav />
<Body />
</UserContext.Provider>
</div>
);
}
}
我们来了解一下它是如何运作的。
请记住,这里有 3 个部分:上下文本身(用 创建React.createContext),以及与它交互的两个组件(Provider和Consumer)。
提供者和消费者是一对
提供者和消费者紧密相连,不可分割。他们只能彼此通信。如果您创建了两个独立的上下文,例如“上下文1”和“上下文2”,那么上下文1的提供者和消费者将无法与上下文2的提供者和消费者通信。
上下文不包含任何状态
请注意,上下文本身没有状态。它仅仅是数据的通道。您必须向上下文传递一个值Provider,而该值会被传递给任何Consumer知道如何查找它的服务(与提供者绑定到同一上下文的消费者)。
创建上下文时,您可以像这样传入一个“默认值”:
const Ctx = React.createContext(yourDefaultValue);
Consumer当它被放置在一个没有上级元素的树状结构中时,它将接收到这个默认值Provider。如果您不传递默认值,则该值将为空undefined。但请注意,这是一个默认值,而不是初始值。上下文不会保留任何内容;它只是分发您传入的数据。
消费者使用渲染属性模式
Redux 的connect函数是一个高阶组件(简称 HoC)。它封装了另一个组件并将 props 传递给它。
相比之下,上下文Consumer期望子组件是一个函数。然后,它会在渲染时调用该函数,并将从其Provider上方某个地方获取的值(或上下文的默认值,或者undefined如果您没有传递默认值)传递给该函数。
提供者接受一个值
只能传递一个值作为valueprop。但请记住,这个值可以是任何内容。实际上,如果您想传递多个值,可以创建一个包含所有值的对象,然后将该对象传递下去。
以上就是 Context API 的基本原理。
上下文 API 具有灵活性
由于创建上下文会给我们提供两个组件(提供者和消费者),我们可以自由地使用它们。以下是一些建议。
将消费者转化为高阶组件
不喜欢在UserContext.Consumer所有需要的地方都加上括号?好吧,这是你的代码!你想怎么做就怎么做。你是个成年人。
如果您希望将值作为 prop 接收,您可以像Consumer这样编写一个简单的包装器:
function withUser(Component) {
return function ConnectedComponent(props) {
return (
<UserContext.Consumer>
{user => <Component {...props} user={user}/>}
</UserContext.Consumer>
);
}
}
然后,你可以重写代码,例如,UserAvatar使用这个新withUser函数:
const UserAvatar = withUser(({ size, user }) => (
<img
className={`user-avatar ${size || ""}`}
alt="user avatar"
src={user.avatar}
/>
));
瞧,上下文就可以像 Redux 那样工作了connect。只是少了自动纯净性。
这里有一个包含此高阶组件的 CodeSandbox 示例。
提供者的保持状态
请记住,上下文的提供者只是一个通道,它本身并不保存任何数据。但这并不妨碍您创建自己的包装器来保存数据。
在上面的例子中,我没有直接App存储数据,所以你只需要理解 Provider 和 Consumer 组件即可。但你可能想创建自己的“状态存储”。你可以创建一个组件来保存状态,并通过上下文传递状态:
class UserStore extends React.Component {
state = {
user: {
avatar:
"https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
name: "Dave",
followers: 1234,
following: 123
}
};
render() {
return (
<UserContext.Provider value={this.state.user}>
{this.props.children}
</UserContext.Provider>
);
}
}
// ... skip the middle stuff ...
const App = () => (
<div className="app">
<Nav />
<Body />
</div>
);
ReactDOM.render(
<UserStore>
<App />
</UserStore>,
document.querySelector("#root")
);
现在,用户数据被妥善地封装在一个独立的组件中,该组件的唯一职责就是处理用户数据。太棒了!App它又可以再次实现无状态了。我觉得这样看起来也更简洁一些。
这是一个包含此 UserStore 的 CodeSandbox 示例。
通过上下文向下传递操作
记住,传递下去的对象Provider可以包含任何你想要的内容。这意味着它可以包含函数。你甚至可以称它们为“动作”。
这里有一个新例子:一个简单的房间,里面有一个灯开关来切换背景颜色——呃,我的意思是灯光。
状态信息保存在 store 中,store 中还有一个用于切换灯光的函数。状态信息和函数都会通过上下文传递下去。
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
// Plain empty context
const RoomContext = React.createContext();
// A component whose sole job is to manage
// the state of the Room
class RoomStore extends React.Component {
state = {
isLit: false
};
toggleLight = () => {
this.setState(state => ({ isLit: !state.isLit }));
};
render() {
// Pass down the state and the onToggleLight action
return (
<RoomContext.Provider
value={{
isLit: this.state.isLit,
onToggleLight: this.toggleLight
}}
>
{this.props.children}
</RoomContext.Provider>
);
}
}
// Receive the state of the light, and the function to
// toggle the light, from RoomContext
const Room = () => (
<RoomContext.Consumer>
{({ isLit, onToggleLight }) => (
<div className={`room ${isLit ? "lit" : "dark"}`}>
The room is {isLit ? "lit" : "dark"}.
<br />
<button onClick={onToggleLight}>Flip</button>
</div>
)}
</RoomContext.Consumer>
);
const App = () => (
<div className="app">
<Room />
</div>
);
// Wrap the whole app in the RoomStore
// this would work just as well inside `App`
ReactDOM.render(
<RoomStore>
<App />
</RoomStore>,
document.querySelector("#root")
);
应该使用 Context 还是 Redux?
既然你已经了解了两种方法——那么你应该使用哪一种呢?嗯,如果说有什么方法能让你的应用开发更出色、更有趣,那就是掌控决策权。我知道你可能只想得到“答案”,但很遗憾,我不得不告诉你,“这要视情况而定”。
这取决于很多因素,比如你的应用规模有多大,或者将来会发展到什么规模。有多少人会参与开发——只有你一个人,还是一个更大的团队?你或你的团队对函数式编程概念(Redux 所依赖的概念,例如不可变性和纯函数)的经验如何?
JavaScript 生态系统中普遍存在的一个有害谬误是竞争观念。这种观念认为每个选择都是零和博弈:如果你使用库 A,就不能使用它的竞争对手库 B。这种观念认为,当一个在某些方面更优秀的新库出现时,它就必须取代现有的库。人们普遍认为一切都必须是非此即彼的,你必须要么选择最新最好的库,要么就被淘汰,和那些老旧的开发者一起被边缘化。
更好的方法是把这些琳琅满目的选择看作一个工具箱。这就好比是在螺丝刀和冲击扳手之间做选择。80% 的情况下,冲击扳手拧螺丝的速度肯定比螺丝刀快。但剩下的 20% 的情况下,螺丝刀反而是更好的选择——可能是因为空间狭小,或者要拧的东西比较易碎。当我买了冲击扳手之后,我并没有立刻扔掉我的螺丝刀,甚至连普通电钻也没扔。冲击扳手并没有取代它们,它只是给了我另一种选择,另一种解决问题的方法。
Context 并不能“取代”Redux,就像 React 不能“取代”Angular 或 jQuery 一样。说实话,我需要快速完成一些事情的时候仍然会使用 jQuery。我有时仍然会使用服务器端渲染的 EJS 模板,而不是启动一个完整的 React 应用。有时候,React 的功能对于当前任务来说过于强大。有时候,Redux 的功能也过于强大。
如今,当 Redux 的功能超出您的需求时,您可以选择使用 Context。
Redux 与 React Context API 的对比最初由 Dave Ceddia 于 2018 年 7 月 17 日发布在Dave Ceddia网站上。
文章来源:https://dev.to/dceddia/redux-vs-the-react-context-api-1nof





