Redux与React Context API



在React 16.3中,添加了新的Context API。 从旧的 Context API在幕后的意义上讲,它是的,大多数人要么不知道它的存在,要么不使用它,因为文档建议不要使用它。

但是,现在Context API是React的完整组成部分,可以使用(不像以前那样正式使用)。

在React 16.3发行之后,立即出现了一些文章,宣称由于新的Context API,Redux死亡。 如果您向Redux询问此事,我想他会回答-“我的死讯被大大夸大了 。”

在本文中,我想讨论一下新的Context API的工作原理,它看起来像Redux的情况,何时可以使用Context而不是Redux的原因,以及为什么Context在每种情况下都不能代替Redux。

如果您只需要Context API的概述,可以点击链接

React应用示例


我假设您已经了解了在React(props&state)中使用状态的原理,但是如果不是这样,我将提供为期5天的免费课程,以帮助您了解它

让我们看一个示例,将我们带到Redux中使用的概念。 我们将从一个简单的React版本开始,然后看看它在Redux中的外观,最后是Context。



在此应用程序中,用户信息显示在两个位置:右上角的导航栏中和主要内容旁边的侧面板中。

(您可能会注意到与Twitter有很多相似之处。这并非巧合!磨练您的React技能的最好方法之一就是复制(创建现有网站/应用程序的副本)

该组件的结构如下所示:



使用纯React(仅支持props),我们需要将用户信息存储在树中足够高的位置,以便可以将其传递给需要它的组件。 在这种情况下,用户信息必须在应用程序中。

然后,为了将有关用户的信息传输到需要它的那些组件,应用程序必须将其传递给Nav和Body。 然后,他们会将其传递给UserAvatar(欢呼!)和补充工具栏。 最后,补充工具栏应将其传递给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")); 


CodeSandbox示例代码

App 初始化包含用户对象的状态 。 在实际的应用程序中,您最有可能从服务器提取此数据并将其保存为呈现状态。

关于道具(“道具钻探”), 这没什么大不了的 。 效果很好。 投掷道具,这是React的理想示例。 但是在编写时,深入了解状态树可能会有些烦人。 如果您必须传送很多道具(而不是一个),则更令人烦恼。

但是,此策略有一个很大的缺点:它在不应连接的组件之间建立了连接。 在上面的示例中,即使Nav不需要它,Nav也应接受“用户”道具并将其传递给UserAvatar。

紧密耦合的组件(例如将道具传递给孩子的组件)更难重用,因为每当在新的地方使用它们时,都必须将它们绑定到新的父母身上。

让我们看看如何改善这一点。

使用Context或Redux之前...


如果您找到一种方法来组合应用程序结构,并利用将props传递给子代的优势,则可以使代码更整洁,而不必求助于props, ContextRedux

在此示例中,儿童道具对于需要通用的组件(例如Nav,Sidebar和Body)是一个很好的解决方案。 另外请注意,您可以将JSX传递给任何道具,而不仅仅是传递给孩子-因此,如果需要多个“插槽”来连接组件,请记住这一点。

这是一个React应用程序的示例,其中Nav,Body和Sidebar带子项并按原样显示它们。 因此,使用该组件的人不必担心传输该组件所需的某些数据。 他可以使用范围内已有的数据简单地显示所需的内容。 此示例还显示了如何使用任何道具来传播孩子。

(感谢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")); 


CodeSandbox示例代码

如果您的应用程序太复杂(比此示例还要复杂!),可能很难理解如何使模板适合于孩子。 让我们看看如何用Redux替换道具转发。

Redux示例


我将快速看一下Redux示例,以便我们可以更好地理解Context的工作原理,因此,如果您对Redux没有清晰的了解,请先阅读我对Redux的介绍 (或观看视频 )。

这是经过重新设计以使用Redux的React应用程序。 用户信息已移至Redux存储,这意味着我们可以使用react-redux connect函数将用户prop直接传递给需要它们的组件。

就摆脱连接性而言,这是一个巨大的胜利。 看一下Nav,Body和Sidebar,您会发现它们不再接收或传输用户道具。 他们不再用道具玩辣土豆。 不再有无用的连接。

减速器在这里无济于事。 这很简单。 关于Redux减速器如何工作以及如何编写使用的不变代码 ,我还有另一件事。

 import React from "react"; import ReactDOM from "react-dom"; //    createStore, connect, and Provider: import { createStore } from "redux"; import { connect, Provider } from "react-redux"; //  reducer       . const initialState = {}; function reducer(state = initialState, action) { switch (action.type) { //    action SET_USER  state. case "SET_USER": return { ...state, user: action.user }; default: return state; } } //  store  reducer'   . const store = createStore(reducer); // Dispatch' action     user. store.dispatch({ type: "SET_USER", user: { avatar: "https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b", name: "Dave", followers: 1234, following: 123 } }); //   mapStateToProps,      state (user) //     `user` prop. const mapStateToProps = state => ({ user: state.user }); //  UserAvatar    connect(),    //`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} /> )); //   UserStats    connect(),    // `user` . 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. const Sidebar = () => ( <div className="sidebar"> <UserStats /> </div> ); //   Body. const Body = () => ( <div className="body"> <Sidebar /> <Content /> </div> ); //  App    ,     . const App = () => ( <div className="app"> <Nav /> <Body /> </div> ); //     Provider, //   connect()    store. ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.querySelector("#root") ); 


CodeSandbox示例代码

现在,您可能想知道Redux如何实现这一魔力。 好厉害 React如何不支持将道具传递到多个级别,Redux可以做到吗?

答案是Redux使用上下文函数React(上下文功能)。 不是现代的Context API(尚未),而是旧的。 除非您正在编写自己的库或知道自己在做什么,否则React文档中不会使用的一种。

上下文类似于每个组件后面的计算机总线:要获得通过它的电源(数据),只需连接即可。 而react-redux connect就是这样做的。

但是,此Redux功能只是冰山一角。 将数据传输到正确的位置是Redux最明显的功能。 您可以立即获得以下其他好处:

连接是一个纯函数

connect会自动使连接的组件“干净”,也就是说,仅当其prop更改时(即,其Redux状态的一部分更改时)才重新渲染它们。 这样可以防止不必要的重新渲染并加快应用程序的速度。

使用Redux轻松调试

Redux为您提供了令人惊讶的调试便利,在编写动作和缩减工具的仪式之间取得了平衡。

使用Redux DevTools扩展,您会自动获得应用程序执行的所有操作的日志。 您可以随时将其打开,查看启动了哪些操作,它们的有效负载是什么以及操作前后的状态。



Redux DevTools提供的另一个重要功能是使用“时间旅行”进行调试,即,您可以单击任何先前的操作并转到当前时间,直到当前时间为止。 之所以起作用,是因为每个动作都以相同的方式更新存储,因此您可以获取已记录状态更新的列表,并在没有任何副作用的情况下播放它们,并最终到达所需的位置。

还有一些工具,例如LogRocket ,基本上可以为您的每个用户提供生产中的永久Redux DevTools。 有错误报告? 没问题 在LogRocket中查看此用户会话,您可以看到他所做的重复以及所执行的操作的重复。 所有这些都可以通过Redux操作流来实现。

使用中间件扩展Redux

Redux支持中间件概念(一个奇特的词,意为“每次发送动作都会运行的函数”)。 编写自己的中间件并不像看起来那样困难,并且允许您使用一些强大的工具。

例如...

  • 是否想每次动作名称以FETCH_开头时发送API请求? 您可以使用中间件来实现。
  • 是否需要一个集中的地方在您的分析软件中记录事件? 中间件是执行此操作的好地方。
  • 是否要阻止某个动作在特定时间点开始? 您可以使用中间件执行此操作,而中间件对于您的应用程序的其余部分是不可见的。
  • 是否要拦截具有JWT令牌的动作并将其自动保存到localStorage? 是的,中间件。

这是一篇很好的文章 ,其中包含有关如何编写Redux中间件的示例。

如何使用React Context API


但是也许您不需要所有这些Redux怪癖。 您可能不需要简单的调试,调整或自动性能改进-您要做的就是轻松地传输数据。 也许您的应用程序很小,或者您只需要快速做一些事情并在以后处理这些细微问题即可。

新的Context API可能很适合您。 让我们看看它是如何工作的。

如果您更愿意观看而不是阅读,我在Egghead上发布了一个快速的Context API 教程 (3:43)。

这是Context API的3个重要组件:

  • React.createContext函数创建上下文
  • 提供程序(返回createContext),用于设置“总线”,
    穿过组件树
  • 消费者(也返回createContext)
    “电动总线”用于数据提取

提供程序与React-Redux中的提供程序非常相似。 它的值可以是您想要的任何值(它甚至可以是Redux商店...但是那很愚蠢)。 最有可能的是,这是一个对象,其中包含您的数据以及您希望对数据执行的任何操作。

使用者的工作有点像React-Redux中的connect函数,它连接到数据并使数据可供使用它的组件使用。

以下是重点内容:

 //     context //    2 : { Provider, Consumer } // ,   ,  UpperCase,  camelCase //  ,          , //        . const UserContext = React.createContext(); // ,     context, //   Consumer. // Consumer   "render props". const UserAvatar = ({ size }) => ( <UserContext.Consumer> {user => ( <img className={`user-avatar ${size || ""}`} alt="user avatar" src={user.avatar} /> )} </UserContext.Consumer> ); // ,      "user prop", //   Consumer    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> ); // ...    ... // ... (      `user`). //  App   context ,  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示例代码

让我们看看它是如何工作的。

记住,我们分为三个部分:上下文本身(使用React.createContext创建)和两个与之交互的组件(Provider和Consumer)。

提供者和消费者一起工作

提供者和消费者是相关且密不可分的。 他们只知道如何相互交流。 如果您创建了两个单独的上下文,例如“ Context1”和“ Context2”,则提供者和使用者Context1将无法与提供者和使用者Context2通信。

上下文不包含状态

注意上下文没有自己的状态 。 这只是您的数据通道。 您必须将值传递给提供者,并且该值将传递给知道如何查找它的任何消费者(提供者与消费者绑定到相同的上下文)。

创建上下文时,可以按以下方式传递“默认”:

 const Ctx = React.createContext(yourDefaultValue); 


默认值是当消费者放置在树中而提供商不在其上方时,消费者将收到的值。 如果不通过,则该值将是不确定的。 请注意,这是默认值 ,而不是初始值。 上下文不保存任何内容。 它只是传播您传递给它的数据。

消费者使用渲染道具模式

connect Redux函数是一个高阶组件(缩写为HoC)。 它包装另一个组件并将道具传递到其中。

相反,消费者期望子组件是一个功能。 然后,它在渲染过程中调用此函数,将它从Provider接收的值传递到它上方的某个位置(上下文的默认值,如果未传递默认值,则为undefined)。

提供者取一个值。

只有一个值,就像道具。 但是请记住,该值可以是任何值。 实际上,如果要向下传递多个值,则必须创建一个具有所有值的对象并将该对象向下传递。

上下文API灵活


由于创建上下文使我们可以使用两个组件(提供者和消费者),因此我们可以根据需要使用它们。 这里有一些想法。

在HOC中包装消费者

不喜欢在需要它的每个地方周围添加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} /> )); 

瞧,上下文可以像连接Redux一样工作。 减去自动清洁度。

这是带有此HOC的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> ); } } // ...    ... const App = () => ( <div className="app"> <Nav /> <Body /> </div> ); ReactDOM.render( <UserStore> <App /> </UserStore>, document.querySelector("#root") ); 

现在,用户数据包含在其自己的组件中,其唯一任务就是这些数据。 好酷 应用可以再次变为无状态。 我认为它看起来更干净一些。

这是此UserStore的CodeSandbox示例。

在上下文中抛出动作

请记住,通过提供者传递的对象可以包含您想要的所有内容。 这意味着它可能包含功能。 您甚至可以为它们命名动作。

这是一个新示例:一个简单的房间,带有一个可以切换背景颜色的开关-哦,我的意思是光线。



状态存储在商店中,该商店还具有灯光开关功能。 状态和功能都通过上下文传递。

 import React from "react"; import ReactDOM from "react-dom"; import "./styles.css"; //  context. const RoomContext = React.createContext(); // ,     //   . class RoomStore extends React.Component { state = { isLit: false }; toggleLight = () => { this.setState(state => ({ isLit: !state.isLit })); }; render() { //  state  onToggleLight action return ( <RoomContext.Provider value={{ isLit: this.state.isLit, onToggleLight: this.toggleLight }} > {this.props.children} </RoomContext.Provider> ); } } //    ,    , //       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> ); //     RoomStore, //           . ReactDOM.render( <RoomStore> <App /> </RoomStore>, document.querySelector("#root") ); 

这是CodeSandbox中完整的工作示例。

毕竟,要使用什么上下文还是Redux?

现在您已经看到了两条路径,哪一条值得使用? 我知道您只是想听听这个问题的答案,但我必须回答-“取决于您”。

这取决于您的应用程序现在有多大或增长速度如何。 有多少人可以从事这项工作-只有您还是一个大型团队? 您或您的团队在使用Redux所依赖的功能概念(例如不变性和纯功能)方面的经验如何。

竞争观念贯穿整个JavaScript生态系统的一个致命错误。 有一种观点认为,每一个选择都是零和游戏:如果使用 A,则不应使用其竞争对手 B。当一个新库比以前的库更好时,应该排挤现有库。 一切都应该是/,或者您必须选择最新和最好的,或者与过去的开发人员一起降级为后台。

最好的方法是通过一个示例和一组工具来查看这一奇妙的选择。 就像在使用螺丝刀或强力螺丝刀之间进行选择。 在80%的情况下,螺丝刀比螺丝刀更容易,更快捷地完成工作。 但是对于其他20%的用户,螺丝刀将是最佳选择(没有足够的空间,或者物品太薄)。 当我购买螺丝起子时,我没有立即扔掉螺丝起子,他没有更换它,只是给了我另一种选择。 解决问题的另一种方法。

上下文不会“替换” Redux,只不过是React“替换” Angular或jQuery。 地狱,当我需要快速做某事时,我仍然使用jQuery。 我有时还是使用服务器端EJS模板而不是部署React应用程序。 有时,React远远超出了完成任务所需的范围。 Redux也是如此。

今天,如果Redux超出了您的需求,则可以使用上下文。

学习React可能很困难-有太多的库和工具!

我的建议 完全忽略它们:)

有关逐步教程,请阅读我的书Pure React

Source: https://habr.com/ru/post/zh-CN419449/


All Articles