Redux vs. React Context API
React 16.3 添加了一个新的 Context API——新的,在这个意义上,旧的 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
。它们又需要再次将其传递给 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"));
这还不算 *糟糕*。它运作良好。但写起来有点烦人。当需要传递很多 props(而不仅仅是一个)时,它会变得更加烦人。
这种“prop drilling”策略还有一个更大的缺点:它会在本应解耦的组件之间产生耦合。在上面的例子中,Nav
需要接受一个“user” prop 并将其传递给 UserAvatar
,即使 Nav
本身并不需要 user
。
紧密耦合的组件(例如转发 props 给子组件的组件)更难重用,因为当你把它们放到新位置时,你必须将它们与新的父组件连接起来。
让我们看看如何用 Redux 来改进它。
Redux 示例
我将快速介绍 Redux 示例,以便我们能更深入地了解 Context 的工作原理,因此如果你对 Redux 不熟悉,请先阅读这篇 Redux 入门教程(或 观看视频)。
这是上面那个 React 应用程序,已重构为使用 Redux。`user` 信息已移至 Redux store,这意味着我们可以使用 react-redux 的 `connect` 函数将 `user` prop 直接注入到需要的组件中。
这在解耦方面是一个巨大的进步。看看 Nav
、Body
和 Sidebar
,你会发现它们不再接受和传递 `user` prop。不再玩 props 的“烫手山芋”。不再有不必要的耦合。
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 }; <span class="nl">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 文档说不要使用的,除非你正在编写库或者知道你在做什么。
Context 就像一条贯穿每个组件的电线总线:要接收它(数据)传递的电力,你只需要插入插头。而 (React-)Redux 的 `connect` 函数正是这样做的。
不过,Redux 的这个功能只是冰山一角。到处传递数据只是 Redux 最*明显的*功能。以下是你可以开箱即用的其他一些好处:
connect
是纯粹的
connect
会自动使连接的组件“纯粹”,这意味着它们只会在 props 改变时重新渲染——也就是说,当它们的 Redux state 片段改变时。这可以防止不必要的重新渲染,并使你的应用程序运行得很快。DIY 方法:创建一个继承自 PureComponent
的类,或自己实现 shouldComponentUpdate
。
Redux 易于调试
编写 actions 和 reducers 的仪式感,换来的是它提供的强大的调试能力。
借助 Redux DevTools 扩展,你可以获得应用程序执行的每个 action 的自动日志。你可以随时打开它,查看哪些 action 被触发了,它们的 payload 是什么,以及 action 发生前后的 state。
Redux DevTools 启用的另一个强大功能是 *时间旅行调试*,即你可以点击任何过去的 action 并跳转到那个时间点,基本上会重放直到并包括那个 action 的所有 action(但不能再往前)。之所以能实现这一点,是因为每个 action 都会*不可变地*更新 state,因此你可以获取一个记录下来的 state 更新列表并重放它们,而不会产生任何不良影响,并最终达到你期望的状态。
还有像 LogRocket 这样的工具,它们基本上为你的每个用户在*生产环境中*提供了始终在线的 Redux DevTools。收到了 bug 报告?太棒了。在 LogRocket 中查找该用户的会话,你就可以看到他们操作的回放,以及确切触发的 action。这一切都通过监听 Redux 的 action 流来实现。
使用中间件自定义 Redux
Redux 支持 *中间件* 的概念,这是一个听起来很复杂但实际上是“每次 dispatch action 时运行的函数”的术语。编写自己的中间件并不像看起来那么难,而且它能实现一些强大的功能。
例如…
- 每次 action 名称以
FETCH_
开头时,都想发起 API 请求?你可以用中间件做到。 - 想有一个集中的地方记录事件到你的分析软件?中间件是一个不错的地方。
- 想在特定时间阻止某些 action 的触发?你可以用中间件做到,对应用程序的其余部分是透明的。
- 想拦截带有 JWT token 的 action 并自动将其保存到 localStorage?是的,中间件可以。
这是一篇不错的文章,介绍了一些 如何编写 Redux 中间件的示例。
如何使用 React Context API
但嘿,也许你不需要 Redux 的所有这些高级功能。也许你不在乎易于调试、自定义或自动性能改进——你只想轻松地传递数据。也许你的应用程序很小,或者你只需要让某件事正常工作,以后再处理那些高级的东西。
React 的新 Context API 可能会满足你的需求。让我们看看它是如何工作的。
我在 Egghead 上发布了一个快速的 Context API 课程,如果你更喜欢观看而不是阅读(3:43)
Context API 有 3 个重要部分
- 用于创建 context 的
React.createContext
函数 - (由
createContext
返回的)Provider
,它建立了一个贯穿组件树的“电线总线” - (同样由
createContext
返回的)Consumer
,它接入“电线总线”以提取数据
Provider
非常类似于 React-Redux 的 Provider
。它接受一个 value
prop,这个 prop 可以是任何你想要的东西(甚至可以是一个 Redux store……但那就太傻了)。它很可能是一个包含你的数据以及你想要对数据执行的任何操作的对象。
Consumer
的工作方式有点像 React-Redux 的 connect
函数,它接入数据并使其可供使用它的组件使用。
以下是重点
<code>// 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> ); } } </code>
让我们来分析一下它是如何工作的。
记住有 3 个部分:context 本身(用 React.createContext
创建),以及与之通信的两个组件(Provider
和 Consumer
)。
Provider 和 Consumer 是成对出现的
Provider 和 Consumer 是绑定在一起的。不可分割的。它们只知道如何 *相互* 通信。如果你创建了两个独立的 contexts,比如“Context1”和“Context2”,那么 Context1 的 Provider 和 Consumer 将无法与 Context2 的 Provider 和 Consumer 通信。
Context 不存储状态
请注意,context *本身不存储状态*。它仅仅是数据的通道。你必须向 Provider
传递一个值,并且该确切的值会传递给任何知道如何查找它的 Consumer
(知道如何查找与 Provider 绑定到同一 context 的 Consumer)。
当你创建 context 时,你可以传递一个“默认值”,如下所示
<code>const Ctx = React.createContext(yourDefaultValue); </code>
这个默认值是当 Consumer
放置在没有任何 Provider
在它之上的组件树中时接收到的值。如果你不传递默认值,则该值将是 undefined
。但是请注意,这是一个*默认*值,而不是*初始*值。Context 不保留任何东西;它仅仅分发你传入的数据。
Consumer 使用 Render Props 模式
Redux 的 `connect` 函数是一个高阶组件(或者简称为 HoC)。它*包装*另一个组件并将 props 传递给它。
相比之下,Context 的 `Consumer` 期望子组件是一个函数。然后它在渲染时调用该函数,将从它上方的 Provider
获取的值(或者 context 的默认值,或者如果你没有传递默认值,则是 undefined
)传递给它。
Provider 接受一个值
只有一个值,作为 value
prop。但请记住,这个值可以是任何东西。实际上,如果你想传递多个值,你会创建一个包含所有值的对象,然后将*该对象*传递下去。
这就是 Context API 的基本原理。
Context API 灵活
由于创建 context 会给我们带来两个组件(Provider 和 Consumer),我们可以根据自己的意愿自由使用它们。这里有几个想法。
将 Consumer 转换为高阶组件
不喜欢在每个需要的地方都添加 UserContext.Consumer
的想法?好吧,这是你的代码!你可以做任何你想做的事。你已经是个成年人了。
如果你更愿意将值作为 prop 接收,你可以围绕 `Consumer` 写一个小的包装器,如下所示
function withUser(Component) { return function ConnectedComponent(props) { return ( <UserContext.Consumer> {user => <Component <span class="err">{...props<span class="err">} user={user}/>} </UserContext.Consumer> ); } }
然后你可以重写,比如 UserAvatar
来使用这个新的 withUser
函数
const UserAvatar = withUser(({ size, user }) => ( <img className={`user-avatar ${size || ""}`} alt="user avatar" src={user.avatar} /> ));
然后,Boom!Context 可以像 Redux 的 `connect` 一样工作。不包含自动纯粹性。
在 Provider 中保持状态
记住,Context 的 Provider 只是一个通道。它不保留任何数据。但这并不妨碍你创建自己的包装器来保存数据。
在上面的例子中,我让 App
来保存数据,这样你只需要理解 Provider + Consumer 组件。但也许你想创建自己的“store”。你可以创建一个组件来保存状态并通过 context 传递它们
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。
通过 Context 传递 Actions
记住,通过 Provider
传递的对象可以包含任何你想要的东西。这意味着它可以包含函数。你甚至可以称它们为“actions”。
这是一个新例子:一个简单的 Room,有一个灯开关来切换背景颜色——呃,我是说灯。
状态保存在 store 中,store 还有一个切换灯的函数。状态和函数都通过 context 传递。
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: <span class="kc">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 并不像 React“取代”Angular 或 jQuery 那样“取代”Redux。说真的,当我需要快速完成某事时,我仍然使用 jQuery。我有时仍然使用服务器渲染的 EJS 模板,而不是启动整个 React 应用程序。有时 React 对你当前的任务来说有点过了。有时 Redux 对你来说也过于复杂。
今天,当 Redux 对你来说过于复杂时,你可以选择 Context。
Redux vs. The React Context API » was originally published by Dave Ceddia at Dave Ceddia on July 17, 2018.