- 原文地址:Redux vs. The React Context API
- 原文做者:Dave Ceddia
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:Xuyuey
- 校对者:Minghao, Baddyo
React 在 16.3 版本里面引入了新的 Context API —— 说它是新的是由于老版本的 context API 是一个幕后的试验性功能,大多数人要么不知道,要么就是依据官方文档所说,尽可能避免使用它。css
可是,如今 Context API 摇身一变成为了 React 中的一等公民,对全部人开放(不像是以前那样,如今是被官方所提倡使用的)。前端
React 16.3 版本一发布,宣称新的 Context API 将要取缔 Redux 的文章在网上铺天盖地而来。可是,若是你去问问 Redux,我认为它会说“那些宣告我会死亡的报道实在是言过其实”。react
在这篇文章中,我想向你们介绍一下新的 Context API 是如何工做的,它与 Redux 的类似之处,什么状况下可使用 Context API 而不是 Redux,以及为何不是全部状况下 Context API 均可以替换 Redux 的缘由。android
若是你只是想了解 Context 的概述,能够跳转到这一节。ios
这里假设你已经了解了 React 的基础知识(props 和 state),可是若是你尚未,你能够参加个人 5 天免费课程,来学习 React 基础知识。git
让咱们看一个可让大多数人接触 Redux 的例子。咱们将从一个单纯的 React 版本开始介绍,而后看看它在 Redux 中的样子,最后是 Context。github
在该应用中用户信息显示在两个位置:导航栏的右上角以及主要内容旁边的侧边栏。ajax
(你可能会注意到它看起来很像 Twitter。这绝对不是碰巧的!磨练 React 技能的最佳方法之一就是经过复制 —— 构建现有应用的副本)。redux
组件结构以下所示:后端
使用纯 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"));
复制代码
在这里,App
初始化 state 时已经包含了 “user” 对象 —— 可是在真实应用中你可能会须要从服务器上获取该数据并将它保存在 state 中,以便渲染。
这种 prop drilling(译者注:属性的向下传递)的方式,并不是糟糕的作法。它工做的还不错。并非全部状况下都不鼓励 “prop drilling”;它是一种完美的有效模式,是支持 React 工做的核心。可是若是组件的层次太深,在你编写的时候就会有点烦人。特别是当你向下传递不止一个属性,而是一大堆的时候,它会变得更加烦人。
然而,这种 “prop drilling” 策略有一个更大的缺点:它会让本应该独立的组件耦合在一块儿。在上面的例子中,Nav
组件须要接收一个 “user” 属性,再将它传递给 UserAvatar
,即便 Nav
中没有任何其它的地方须要用到 user
属性。
紧密耦合的组件(就像那些向它们的子组件传递属性的组件)更加难以被复用,由于不管何时你要在新的地方使用它,你都必须将它们和新的父组件联系起来。
让咱们来看看如何改进。
若是你能够找到一种方法来合并应用的结构,并利用好 children
属性,这样,无需借助深层次的 prop drilling 或是 Context,或是 Redux,你也可让代码结构变得更清晰。
对于那些须要使用通用占位符的组件,例如本例中的 Nav
、Sidebar
和 Body
,children 属性是一个很好的解决方案。还要知道,你能够传递 JSX 元素给任意属性,并不只仅是 “children” —— 因此若是你想使用不止一个 “slot” 来插入组件时,请记住这一点。
这个例子中 Nav
、Sidebar
和 Body
接收 children,而后按照它们的样子渲染出来。这样,组件的使用者不用担忧传递给组件的特定数据 —— 他只须要使用组件内定义的数据,并按照组件的原始需求简单地渲染组件。这个例子中还说明了怎样使用任意属性传递 children。
(感谢 Dan Abramov 的这个建议!)
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>
);
// 接收并渲染 children
const Nav = ({ children }) => (
<div className="nav">
{children}
</div>
);
const Content = () => (
<div className="content">main content here</div>
);
const Sidebar = ({ children }) => (
<div className="sidebar">
{children}
</div>
);
// Body 须要一个 sidebar 和 content,可是能够按照这样的方式写,
// 它们能够是任意属性
const Body = ({ sidebar, content }) => (
<div className="body">
<Sidebar>{sidebar}</Sidebar>
{content}
</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>
<UserAvatar user={user} size="small" />
</Nav>
<Body
sidebar={<UserStats user={user} />}
content={<Content />}
/>
</div>
);
}
}
ReactDOM.render(<App />, document.querySelector("#root"));
复制代码
若是你的应用太复杂了(比这个例子更复杂!),也许很难弄清楚如何调整 children
模式。让咱们来看看如何用 Redux 替换 prop drilling。
这里我会快速过一下 Redux 示例,这样咱们能够多用点时间深刻地了解 Context 的工做原理,因此若是你不是很清楚 Redux,能够先去看看个人 Redux 简介(或者观看视频)。
咱们使用的仍是上面的 React 应用,这里咱们将它重构为 Redux 版本。user
信息被移入了 Redux 存储,这意味着咱们可使用 react-redux 的 connect
函数,直接将 user
属性注入到须要它的组件中。
这在解耦方面是一个巨大的胜利。看看 Nav
、Sidebar
和 Body
,你会发现它们再也不接收和向下传递 user
属性了。不用再玩 props 这块烫手山芋了。固然也不会有更多没必要要的耦合。
这里的 reducer 没有作不少工做;很是的简单。我在其它地方有更多关于 Redux Reducer 如何工做以及如何编写其中的不可变代码的文章,你能够看看。
import React from "react";
import ReactDOM from "react-dom";
// 咱们须要 createStore、connect 和 Provider:
import { createStore } from "redux";
import { connect, Provider } from "react-redux";
// 建立一个初始 state 为空的 reducer
const initialState = {};
function reducer(state = initialState, action) {
switch (action.type) {
// 响应 SET_USER 行为并更新
// 相应的 state
case "SET_USER":
return {
...state,
user: action.user
};
default:
return state;
}
}
// 使用 reducer 建立 store
const store = createStore(reducer);
// 触发设置 user 的行为
// (由于 user 初始化时为空)
store.dispatch({
type: "SET_USER",
user: {
avatar: "https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
name: "Dave",
followers: 1234,
following: 123
}
});
// 函数 mapStateToProps 从 state 对象中提取 user 值
// 并将它做为 `user` 属性传递
const mapStateToProps = state => ({
user: state.user
});
// connect() UserAvatar 以便它能够直接接收 `user` 属性,
// 而无需从上层组件中获取
// 也能够把它分红下面 2 个变量:
// 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 以便它能够直接接收 `user` 属性,
// 而无需从上层组件中获取
// (一样使用 mapStateToProps 函数)
const UserStats = connect(mapStateToProps)(({ user }) => (
<div className="user-stats">
<div>
<UserAvatar />
{user.name}
</div>
<div className="stats">
<div>{user.followers} Followers</div>
<div>Following {user.following}</div>
</div>
</div>
));
// Nav 再也不须要知道 `user` 属性
const Nav = () => (
<div className="nav">
<UserAvatar size="small" />
</div>
);
const Content = () => (
<div className="content">main content here</div>
);
// Sidebar 也再也不须要知道 `user` 属性
const Sidebar = () => (
<div className="sidebar">
<UserStats />
</div>
);
// body 一样不须要知道 `user` 属性
const Body = () => (
<div className="body">
<Sidebar />
<Content />
</div>
);
// App 再也不须要保存 state,
// 因此能够把它写成一个无状态组件
const App = () => (
<div className="app">
<Nav />
<Body />
</div>
);
// 用 Provider 包裹整个 App,
// 以便 connect() 能够链接到 store
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector("#root")
);
复制代码
如今你可能想知道 Redux 如何能实现这样神奇的功能。“想知道”是一件好事情。React 不支持跨越多个层级传递属性,那为什么 Redux 能够实现呢?
答案是 Redux 使用了 React 的 context(上下文)特性。不是如今咱们说的 Context API(还不是)—— 而是旧的那个。就是 React 文档说不要使用的那个,除非你在写库文件或者你知道在作什么。
Context 就像一个在每一个组件背后运行的电子总线:要接收它传递的电源(数据),你只须要插入插头就好。而(React-)Redux 的 connect
函数就是作这件事的。
不过,Redux 的这个功能只是冰山一角。能够在全部地方传递数据只是 Redux 最明显的功能。如下是你能够开箱即用的其余一些好处:
connect
使你的组件很纯粹connect
可让被链接的组件很“纯粹”,意味着它们只须要在本身的属性改变时从新渲染 —— 也就是在它们的 Redux 状态切片发生改变时。这能够防止没必要要的重复渲染,使你的应用可以快速运行。DIY 方法:建立一个类继承 PureComponent
,或是本身实现 shouldComponentUpdate
。
虽然写 action 和 reducer 有一点复杂,可是咱们可使用它提供给咱们的强大调试能力来平衡这一点。
使用 Redux DevTools 扩展,应用程序执行的每一个操做都会被自动记录下来。你能够随时打开它,查看触发的操做,有效负载是什么,以及操做发生先后的 state。
Redux DevTools 提供了另外一个很棒的功能 —— time travel debugging(时间旅行调试),也就是说,你能够点击任何过去的动做并跳转到那个时间点,它基本上能够重放每个动做,包括如今的那个,但不包括尚未触发的动做。其原理是每一个动做都会不可变地更新 state,因此你能够拿到记录了 state 更新的列表并重放它们,跳转到你想去的地方,并且没有任何反作用。
并且目前有像 LogRocket 这样的工具,能够为你的每个用户在生产环境中提供一个永远在线的 Redux DevTools。有 bug 报告?不要紧。在 LogRocket 中查找该用户的会话,你能够看到他们所作的全部事情以及确切触发的操做。这一切均可以经过使用 Redux 的操做流来实现。
Redux 支持中间件(middleware)的概念,表明着“每次调度某个动做以前都会运行的函数”。编写本身的中间件并不像看起来那么难,它能够实现一些强大的功能。
例如……
FETCH_
开头的操做中提交 API 请求?你可使用中间件。这里有一篇很好的文章,里面有一些如何编写 Redux 中间件的示例。
可是,也许你不须要 Redux 全部那些花哨的功能。也许你不关心简单调试、自定义或是性能的自动化提高 —— 你想作的只是轻松地传递数据。也许你的应用很小,或者如今你只是须要让应用运转起来,之后再去考虑那些花哨的东西。
React 的新 Context API 可能符合你的要求。让咱们看看它是如何工做的。
若是你更愿意看视频(时长 3:43)而不是读文章,我在 Egghead 上发布了一个简短的 Context API 课程:
Context API 中有 3 个重要的部分:
React.createContext
函数:建立上下文Provider
(由 createContext
返回):在组件树中构建“电子总线”Consumer
(一样由 createContext
返回):接入“电子总线”来获取数据这里的 Provider
和 React-Redux 的 Provider
很是类似。它接收一个 value
属性,这个属性能够是任何你想要的东西(甚至能够是一个 Redux store……可是这很傻)。它极可能是一个对象,包括你的数据以及你但愿对数据执行的操做。
这里的 Consumer
工做方式有点像 React-Redux 的 connect
函数,接收数据以供组件使用。
如下是重点:
// 在最开始,咱们建立了一个新的上下文
// 它是一个拥有两个属性 { Provider, Consumer } 的对象
// 注意这里用的是 UpperCase 命名,不是 camelCase
// 这很重要,由于咱们一会要以组件的方式使用它
// 而组件的名称必须以大写字母开头
const UserContext = React.createContext();
// 下面是须要从上下文中获取数据的组件
// 能够经过使用 UserContext 的 Consumer 属性
// Consumer 使用的是 "render props" 模式
const UserAvatar = ({ size }) => (
<UserContext.Consumer>
{user => (
<img
className={`user-avatar ${size || ""}`}
alt="user avatar"
src={user.avatar}
/>
)}
</UserContext.Consumer>
);
// 注意咱们再也不须要 'user' 属性了
// 由于 Consumer 能够直接从上下文中获取
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>
);
// …… 全部其它的组件 ……
// ……(就是那些不会用到 `user` 的组件)……
// 在最下面,App 的内部
// 咱们用 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>
);
}
}
复制代码
这里是 CodeSandbox 中的完整示例。
让咱们来看看它是如何工做的。
记住有 3 个部分:上下文自己(由 React.createContext
建立),以及和它对话的两个组件(Provider
和 Consumer
)。
Provider 和 Consumer 被捆绑在一块儿。如影随行。并且它们只知道如何和对方对话。若是你建立两个单独的上下文,例如 “Context1” 和 “Context2”,那么 Context1 的 Provider 和 Consumer 是不可能和 Context2 的 Provider 和 Consumer 通讯的。
注意上下文没有本身的 state。它只是数据的管道。你必须将值传递给 Provider
,而后这个确切的值会被传递给任何知道如何获取它的 Consumer
(Consumer 和 Provider 绑定的是同一个上下文)。
建立上下文时,能够传入一个“默认值”,以下所示:
const Ctx = React.createContext(yourDefaultValue);
复制代码
当 Consumer
被放在一个没有 Provider
包裹的树上时,它会收到这个默认值。若是你没有传入默认值,这个值会为 undefined
。但要注意这是默认值,而不是初始值。上下文不保留任何内容;它只是分发你传入的数据。
Redux 的 connect
函数是一个高阶组件(或简称 HoC)。它包裹另一个组件,并将 props 传递给它。
上下文的 Consumer
则相反,它指望子组件是一个函数。而后它在渲染的时候调用这个函数,将它从包裹它的 Provider
上得到的值(或上下文的默认值,若是你没有传入默认值,那也多是 undefined
)传给子组件。
它接收 value
属性,仅此一个值。但请记住这个值能够是任何东西。在实践中,若是你想要向下传递多个值,你必须建立一个包含这些值的对象,再将这个对象传递下去。
这几乎是 Context API 的最核心的东西。
由于建立上下文为咱们提供了两个可使用的组件(Provider 和 Consumer),所以咱们能够随意使用它们。这里有几个想法。
不喜欢在每一个须要使用 UserContext.Consumer
的地方都添加它的用法?嗯,这是你的代码!你能够作任何你想作的事。你是个成年人了。
若是你更愿意接收一个做为属性的值,你能够为 Consumer
写一个包裹器,像下面这样:
function withUser(Component) {
return function ConnectedComponent(props) {
return (
<UserContext.Consumer>
{user => <Component {...props} user={user}/>}
</UserContext.Consumer>
);
}
}
复制代码
而后你能够重写你的代码,好比使用了新 withUser
函数的 UserAvatar
组件:
const UserAvatar = withUser(({ size, user }) => (
<img className={`user-avatar ${size || ""}`} alt="user avatar" src={user.avatar} /> )); 复制代码
BOOM,上下文能够像 Redux 的 connect
那样工做。让你的组件很纯粹。
记住,上下文的 Provider 只是一个管道。它不保留任何数据。但这并不能阻止你制做本身的包裹器来保存数据。
在上面的示例中,我用 App
保存数据,所以这里你惟一须要了解的新事物就是这个 Provider + Consumer 组件。但也许你想写一个本身的 “store”,等等。你能够建立一个组件来保存数据,并经过上下文传递它们。
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> ); } } // ……略过中间的内容…… const App = () => ( <div className="app"> <Nav /> <Body /> </div> ); ReactDOM.render( <UserStore> <App /> </UserStore>, document.querySelector("#root") ); 复制代码
如今,你的用户数据被很好地包含在它本身的组件中了,这个组件惟一关注的就是用户数据。很棒。App
又能够再次变成无状态组件了。我认为它看起来更整洁了。
这里是带有这个 UserStore 的 CodeSandbox 示例。
记住经过 Provider 传递的对象能够包含你想要的任何东西。这意味着它能够包含函数。你甚至能够称之为“操做(action)”。
这是一个新例子:一个简单的房间,带有一个能够切换背景颜色的开关 —— 抱歉,个人意思是灯光。
State 被保存在 store 中,store 中还有切换灯光的函数。State 和函数都经过上下文传递。
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
// 简单的空上下文
const RoomContext = React.createContext();
// 一个组件
// 惟一的工做就是管理 Room 的 state
class RoomStore extends React.Component {
state = {
isLit: false
};
toggleLight = () => {
this.setState(state => ({ isLit: !state.isLit }));
};
render() {
// 传递 state 和 onToggleLight 操做
return (
<RoomContext.Provider
value={{
isLit: this.state.isLit,
onToggleLight: this.toggleLight
}}
>
{this.props.children}
</RoomContext.Provider>
);
}
}
// 从 RoomContext 中接收灯光的 state
// 以及切换灯光的函数
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>
);
// 用 RoomStore 包裹整个 App
// 它能够像在 `App` 内那样工做
ReactDOM.render(
<RoomStore>
<App />
</RoomStore>,
document.querySelector("#root")
);
复制代码
这里是 CodeSandbox 中的完整示例。
既然你已经看过两种方式了 —— 那你应该使用哪一种方式呢?好吧,这里有一件事会让你的应用更好而且写起来更有趣,那就是作决策。我知道你可能只想要“答案”,但我很遗憾地告诉你,“这视状况而定”。
这取决于你的应用程序有多大或将会变成多大。有多少人会参与其中 —— 只有你仍是有更大的团队?你或你的团队对于 Redux 所依赖的函数式概念(如不变性和纯函数)的经验。
在 JavaScript 生态系统中存在的一个巨大的恶性谬论是竞争的概念。有观点认为,每一次选择都是一个零和游戏;若是你使用库 A,你就不能使用它的竞争对手库 B。这个想法是说当出现了一个在某种程度上更好的新库,它必须取代现有的库。这是一种「或者……或者……」的感受,你必须选择目前最好的库,或者和过去的人一块儿使用以前的库。
更好的方法是拥有一个像是你的工具箱同样的东西,能够把你的选择项都放进去。就像是选择使用螺丝刀仍是冲击钻。对于 80% 的工做,使用冲击钻拧螺丝都比螺丝刀更快。但对于另外的 20%,螺丝刀其实是更好的选择 —— 或许由于空间比较狭小,或是物品很精细。当我有一个冲击钻时,我并无当即扔掉个人螺丝刀,甚至是个人非冲击钻。冲击钻没有取代它们,它只是给了我另一种选择。另一种解决问题的方法。
React 会“替代” Angular 或 jQuery,但 Context 不会像这样“替代” Redux。哎呀,当我须要快速完成一些事情时,我仍然会使用 jQuery。我有时仍会使用服务器渲染的 EJS 模板,而不是使用整个 React 应用程序。有时 React 比你手上的任务需求更庞大。有时 Redux 里也会有你不须要的功能。
如今,当 Redux 超出你的需求时,你可使用 Context。
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。