在React Development中使用RxJS来管理应用程序状态

该材料的作者(我们今天将其翻译发表)说,他想在此演示开发使用RxJS的简单React应用程序的过程。 据他说,他不是RxJS方面的专家,因为他从事该库的研究并且不拒绝知识渊博的人的帮助。 它的目标是引起读者对创建React应用程序的替代方法的关注,以激发读者进行独立研究。 该材料不能称为RxJS简介。 这里显示了在React开发中使用该库的多种方法之一。

图片

一切如何开始


最近, 我的客户启发了我学习如何使用RxJS管理React应用程序的状态。 当我审核此客户的应用程序代码时,他想了解我对他如何开发该应用程序的看法,因为在此之前,它仅使用React的本地状态。 该项目达到了完全依靠React不合理的地步。 首先,我们讨论了使用Redux或MobX作为管理应用程序状态的更好方法。 我的客户为每种技术创建了一个原型。 但是他并没有止步于这些技术,而是创建了一个使用RxJS的React应用程序的原型。 从那一刻起,我们的对话变得更加有趣。

该应用程序是加密货币的交易平台。 它有许多小部件,其数据是实时更新的。 除其他外,此应用程序的开发人员必须解决以下难题:

  • 管理多个(异步)数据请求。
  • 实时更新控制面板上的大量小部件。
  • 解决小部件和数据连接问题的方法,因为某些小部件不仅需要来自某些特殊来源的数据,而且还需要来自其他小部件的数据。

结果,开发人员面临的主要困难与React库本身无关,此外,我可以在这一方面帮助他们。 主要问题是使系统的内部机制正确运行,这些内部机制链接了由React工具创建的加密货币数据和接口元素。 正是在这一领域,RxJS的功能派上了用场,他们展示给我的原型看起来非常有前途。

在React中使用RxJS


假设我们有一个应用程序,该应用程序在执行了一些本地操作之后,会向第三方API发出请求。 它允许您按文章搜索。 在执行请求之前,我们需要获取用于构成此请求的文本。 特别是,我们使用此文本形成用于访问API的URL。 这是实现此功能的React组件的代码

import React from 'react'; const App = ({ query, onChangeQuery }) => (  <div>    <h1>React with RxJS</h1>    <input      type="text"      value={query}      onChange={event => onChangeQuery(event.target.value)}    />    <p>{`http://hn.algolia.com/api/v1/search?query=${query}`}</p>  </div> ); export default App; 

该组件缺少状态管理系统。 query属性的状态不会存储在任何地方, onChangeQuery函数也不会更新状态。 在常规方法中,这样的组件配备有本地状态管理系统。 看起来像这样:

 class App extends React.Component { constructor(props) {   super(props);   this.state = {     query: '',   }; } onChangeQuery = query => {   this.setState({ query }); }; render() {   return (     <div>       <h1>React with RxJS</h1>       <input         type="text"         value={this.state.query}         onChange={event =>           this.onChangeQuery(event.target.value)         }       />       <p>{`http://hn.algolia.com/api/v1/search?query=${         this.state.query       }`}</p>     </div>   ); } } export default App; 

但是,这不是我们这里要讨论的方法。 相反,我们希望使用RxJS建立应用程序状态管理系统。 让我们看看如何使用高阶组件(HOC)来执行此操作。

如果愿意,您可以在App组件中实现类似的逻辑,但是您很可能在应用程序工作的某个时候决定以适合重用的HOC形式设计这样的组件。

React和RxJS顶级组件


我们将弄清楚如何使用RxJS来管理React应用程序的状态,并为此使用一个更高阶的组件。 相反,可以实现render props template 。 因此,如果您不想为此目的创建高阶组件,则可以使用可观察到的高阶组件mapPropsStream()mapPropsStream()componentFromStream() 。 但是,在本指南中,我们将自行完成所有操作。

 import React from 'react'; const withObservableStream = (...) => Component => { return class extends React.Component {   componentDidMount() {}   componentWillUnmount() {}   render() {     return (       <Component {...this.props} {...this.state} />     );   } }; }; const App = ({ query, onChangeQuery }) => ( <div>   <h1>React with RxJS</h1>   <input     type="text"     value={query}     onChange={event => onChangeQuery(event.target.value)}   />   <p>{`http://hn.algolia.com/api/v1/search?query=${query}`}</p> </div> ); export default withObservableStream(...)(App); 

虽然高阶组件RxJS不执行任何操作。 它仅将自己的状态和属性传输到输入组件,该组件计划在其帮助下进行扩展。 如您所见,最后,一个高阶组件将参与管理React的状态。 但是,将从观察到的流量中获得此状态。 在开始实现HOC并将其与App组件一起使用之前,我们需要安装RxJS:

 npm install rxjs --save 

现在,让我们开始使用一个高阶组件并实现其逻辑:

 import React from 'react'; import { BehaviorSubject } from 'rxjs'; ... const App = ({ query, onChangeQuery }) => ( <div>   <h1>React with RxJS</h1>   <input     type="text"     value={query}     onChange={event => onChangeQuery(event.target.value)}   />   <p>{`http://hn.algolia.com/api/v1/search?query=${query}`}</p> </div> ); const query$ = new BehaviorSubject({ query: 'react' }); export default withObservableStream( query$, {   onChangeQuery: value => query$.next({ query: value }), } )(App); 

App组件本身不会更改。 我们只是将两个参数传递给一个高阶组件。 我们描述它们:

  • 观察对象。 query参数是一个具有初始值的可观察对象,但是随着时间的推移,它还会返回新值(因为这是BehaviorSubject )。 任何人都可以订阅此可观察对象。 这是RxJS文档对BehaviorSubject类型的对象的描述:“ Subject对象的一个​​选项是使用“当前值”概念的BehaviorSubject对象。 它存储传递给其订阅者的最后一个值,当新的观察者订阅它时,它立即从BehaviorSubject接收此“当前值”。 这样的对象非常适合呈现数据,随着时间的推移,其中的新部分会出现。”
  • 用于为观察对象(触发)发布新值的系统。 通过HOC传递给App组件的onChangeQuery()函数是一个常规函数,它将下一个值传递给观察到的对象。 此功能是在对象中转移的,因为可能有必要将对观察到的对象执行某些动作的几个功能转移到高阶组件。

创建观察对象并订阅之后,请求流程应工作。 但是,直到现在,高阶组件本身对我们来说似乎还是一个黑匣子。 我们意识到:

 const withObservableStream = (observable, triggers) => Component => { return class extends React.Component {   componentDidMount() {     this.subscription = observable.subscribe(newState =>       this.setState({ ...newState }),     );   }   componentWillUnmount() {     this.subscription.unsubscribe();   }   render() {     return (       <Component {...this.props} {...this.state} {...triggers} />     );   } }; }; 

高阶组件接收一个可观察对象和一个带有触发器的对象(也许可以从RxJS词典中将包含函数的对象称为更成功的术语),以函数签名表示。

触发器仅通过HOC传递到输入组件。 这就是App组件直接接收onChangeQuery()函数的原因,该函数直接与被观察对象一起使用,并向其传递新值。

被观察的对象使用componentDidMount()生命周期方法进行签名,并使用componentDidMount()方法取消订阅。 需要取消订阅以防止内存泄漏 。 在观察对象的预订中,该函数仅使用this.setState()命令将所有传入数据从流发送到本地React状态存储。

让我们对App组件进行一些小的更改,这将消除如果高阶组件未设置query属性的初始值时发生的问题。 如果不这样做,那么在工作开始时, query属性将变为undefined 。 由于此更改,此属性将获得默认值。

 const App = ({ query = '', onChangeQuery }) => ( <div>   <h1>React with RxJS</h1>   <input     type="text"     value={query}     onChange={event => onChangeQuery(event.target.value)}   />   <p>{`http://hn.algolia.com/api/v1/search?query=${query}`}</p> </div> ); 

解决此问题的另一种方法是在高阶组件中设置query的初始状态:

 const withObservableStream = ( observable, triggers, initialState, ) => Component => { return class extends React.Component {   constructor(props) {     super(props);     this.state = {       ...initialState,     };   }   componentDidMount() {     this.subscription = observable.subscribe(newState =>       this.setState({ ...newState }),     );   }   componentWillUnmount() {     this.subscription.unsubscribe();   }   render() {     return (       <Component {...this.props} {...this.state} {...triggers} />     );   } }; }; const App = ({ query, onChangeQuery }) => ( ... ); export default withObservableStream( query$, {   onChangeQuery: value => query$.next({ query: value }), }, {   query: '', } )(App); 

如果您现在尝试此应用程序,则输入框应能按预期工作。 App组件以属性的形式从HOC接收仅query状态和用于更改状态的onChangeQuery函数。

尽管RxJS的可观察对象可以获取和更改状态,尽管事实上在高阶组件内部使用了内部React状态存储。 对于从观察对象的预订直接向扩展组件( App )的属性订阅流数据的问题,我找不到明显的解决方案。 这就是为什么我必须以中间层的形式使用React的局部状态的原因,此外,在导致重新渲染的原因方面,这很方便。 如果您知道达成相同目标的另一种方法-您可以在评论中分享。

在React中组合观察到的对象


让我们创建第二个值流,就像query属性一样,可以在App组件中使用它。 稍后,我们将使用这两个值,并使用另一个可观察对象处理它们。

 const SUBJECT = { POPULARITY: 'search', DATE: 'search_by_date', }; const App = ({ query = '', subject, onChangeQuery, onSelectSubject, }) => ( <div>   <h1>React with RxJS</h1>   <input     type="text"     value={query}     onChange={event => onChangeQuery(event.target.value)}   />   <div>     {Object.values(SUBJECT).map(value => (       <button         key={value}         onClick={() => onSelectSubject(value)}         type="button"       >         {value}       </button>     ))}   </div>   <p>{`http://hn.algolia.com/api/v1/${subject}?query=${query}`}</p> </div> ); 

如您所见,在构造用于访问API的URL时,可以使用subject参数来优化请求。 即,可以根据它们的受欢迎程度或出版日期来搜索这些材料。 接下来,创建另一个可观察对象,该对象可用于更改subject参数。 此可观察值可用于将App组件与更高级别的组件关联。 否则,传递给App组件的属性将不起作用。

 import React from 'react'; import { BehaviorSubject, combineLatest } from 'rxjs/index'; ... const query$ = new BehaviorSubject({ query: 'react' }); const subject$ = new BehaviorSubject(SUBJECT.POPULARITY); export default withObservableStream( combineLatest(subject$, query$, (subject, query) => ({   subject,   query, })), {   onChangeQuery: value => query$.next({ query: value }),   onSelectSubject: subject => subject$.next(subject), }, )(App); 

onSelectSubject()触发器不是新的。 通过按钮,它可用于在两个subject状态之间切换。 但是,观察到的转移到高阶分量的对象是新事物。 它使用combineLatest()函数合并两个(或更多)观察到的流中最后返回的值。 订阅观察到的对象后,如果任何值( querysubject )发生更改,则订户将接收到两个值。

它的最后一个参数是对combineLatest()函数实现的机制的补充。 在这里,您可以设置由观察对象生成的值的返回顺序。 在我们的情况下,我们需要将它们表示为一个对象。 与以前一样,这将允许它们在高阶组件中进行分解并将其写入本地React状态。 由于我们已经具有必要的结构,因此可以省略包装观察到的query对象的对象的步骤。

 ... const query$ = new BehaviorSubject('react'); const subject$ = new BehaviorSubject(SUBJECT.POPULARITY); export default withObservableStream( combineLatest(subject$, query$, (subject, query) => ({   subject,   query, })), {   onChangeQuery: value => query$.next(value),   onSelectSubject: subject => subject$.next(subject), }, )(App); 

源对象{ query: '', subject: 'search' }以及由观察对象的组合流返回的所有其他对象,适合于以更高阶的成分对其进行分解并将相应的值写入本地React状态。 与以前一样,更新状态后,将执行渲染。 启动更新的应用程序时,您应该能够使用输入字段和按钮来更改两个值。 更改的值会影响用于访问API的URL。 即使这些值中只有一个发生变化,另一个值combineLatest()保留其最后一个状态,因为combineLatest()函数始终会combineLatest()从观察到的流发出的最新值。

React中的Axios和RxJS


现在,在我们的系统中,用于访问API的URL是基于来自组合可观察对象的两个值构造的,该对象包括两个其他可观察对象。 在本节中,我们将使用URL从API加载数据。 您可能可以使用React数据加载系统 ,但是使用RxJS可观察对象时,您需要向我们的应用程序添加另一个可观察流。

在开始研究下一个观察到的对象之前,请安装axios 。 这是我们用来将数据从流加载到程序中的库。

 npm install axios --save 

现在,假设我们有App组件应输出的一系列文章。 在这里,作为相应默认参数的值,我们使用一个空数组,其作用与其他参数相同。

 ... const App = ({ query = '', subject, stories = [], onChangeQuery, onSelectSubject, }) => ( <div>   ...   <p>{`http://hn.algolia.com/api/v1/${subject}?query=${query}`}</p>   <ul>     {stories.map(story => (       <li key={story.objectID}>         <a href={story.url || story.story_url}>           {story.title || story.story_title}         </a>       </li>     ))}   </ul> </div> ); 

对于列表中的每篇文章,由于我们访问的API不一致,因此使用了后备值。 现在-最有趣的是-实现了一个新的可观察对象,该对象负责将数据加载到将其可视化的React应用程序中。

 import React from 'react'; import axios from 'axios'; import { BehaviorSubject, combineLatest } from 'rxjs'; import { flatMap, map } from 'rxjs/operators'; ... const query$ = new BehaviorSubject('react'); const subject$ = new BehaviorSubject(SUBJECT.POPULARITY); const fetch$ = combineLatest(subject$, query$).pipe( flatMap(([subject, query]) =>   axios(`http://hn.algolia.com/api/v1/${subject}?query=${query}`), ), map(result => result.data.hits), ); ... 

同样,一个新的可观察对象是subjectquery可观察对象的组合,因为要构造用于访问API以加载数据的URL,我们需要两个值。 在被观察对象的pipe()方法中,我们可以使用所谓的“ RxJS运算符”来对值执行某些操作。 在这种情况下,我们映射了查询中放置的两个值,axios用来获取结果。 在这里,我们使用flatMap()运算符而不是map()来访问成功解析的flatMap()的结果,而不是返回的Promise本身。 结果,在订阅此新的可观察对象之后,每当从其他可观察对象向系统输入新的subjectquery值时,都会执行新的query ,并且结果将在预订功能中。

现在,我们再次可以为高阶分量提供一个新的可观察的分量。 我们拥有可使用的combineLatest()函数的最后一个参数,因此可以将其直接映射到一个名为combineLatest()的属性。 最后,这代表了如何在App组件中使用这些数据。

 export default withObservableStream( combineLatest(   subject$,   query$,   fetch$,   (subject, query, stories) => ({     subject,     query,     stories,   }), ), {   onChangeQuery: value => query$.next(value),   onSelectSubject: subject => subject$.next(subject), }, )(App); 

这里没有触发,因为被观察对象被其他两个被观察流间接激活。 每次更改输入字段( query )中的值或单击按钮( subject )时,这都会影响观察到的fetch对象,该对象包含两个流中的最新值。

但是,也许我们不需要每次更改输入字段中的值都会影响观察到的fetch对象。 此外,如果该值为空字符串,则我们不希望fetch受到影响。 这就是为什么我们可以使用debounce语句扩展可观察query对象的原因,从而消除了过于频繁的查询更改。 即,由于这种机制,仅在前一事件之后的预定时间之后才接受新事件。 另外,我们在这里使用filter运算符,如果query字符串为空,它将过滤掉流事件。

 import React from 'react'; import axios from 'axios'; import { BehaviorSubject, combineLatest, timer } from 'rxjs'; import { flatMap, map, debounce, filter } from 'rxjs/operators'; ... const queryForFetch$ = query$.pipe( debounce(() => timer(1000)), filter(query => query !== ''), ); const fetch$ = combineLatest(subject$, queryForFetch$).pipe( flatMap(([subject, query]) =>   axios(`http://hn.algolia.com/api/v1/${subject}?query=${query}`), ), map(result => result.data.hits), ); ... 

debounce语句完成将数据输入字段的工作。 但是,当按钮单击subject值时,应立即执行该请求。

现在,首次显示App组件时看到的querysubject的初始值与从观察对象的初始值获得的初始值不同:

 const query$ = new BehaviorSubject('react'); const subject$ = new BehaviorSubject(SUBJECT.POPULARITY); 

subjectsubject编写的, query为空字符串。 这是由于以下事实:我们将这些值作为默认参数提供,用于重构签名中App组件的功能。 这样做的原因是因为我们需要等待可观察的fetch对象执行的初始请求。 由于我不完全知道如何立即从可观察的query和高阶组件中的subject对象获取值以将其写入本地状态,因此我决定再次为高阶组件设置初始状态。

 const withObservableStream = ( observable, triggers, initialState, ) => Component => { return class extends React.Component {   constructor(props) {     super(props);     this.state = {       ...initialState,     };   }   componentDidMount() {     this.subscription = observable.subscribe(newState =>       this.setState({ ...newState }),     );   }   componentWillUnmount() {     this.subscription.unsubscribe();   }   render() {     return (       <Component {...this.props} {...this.state} {...triggers} />     );   } }; }; 

现在可以将初始状态作为第三个参数提供给高阶组件。 将来,我们可以删除App组件的默认设置。

 ... const App = ({ query, subject, stories, onChangeQuery, onSelectSubject, }) => ( ... ); export default withObservableStream( combineLatest(   subject$,   query$,   fetch$,   (subject, query, stories) => ({     subject,     query,     stories,   }), ), {   onSelectSubject: subject => subject$.next(subject),   onChangeQuery: value => query$.next(value), }, {   query: 'react',   subject: SUBJECT.POPULARITY,   stories: [], }, )(App); 

现在让我担心的是,初始状态也在观察对象query$subject$的声明中设置。 这种方法容易出错,因为观察对象的初始化和高阶组件的初始状态共享相同的值。 如果相反,是从较高阶组件中的观察对象中提取初始值以设置初始状态,我会更喜欢它。 也许本材料的读者之一将能够在有关如何执行此操作的评论中分享建议。

我们在此处进行的软件项目的代码可以在此处找到。

总结


本文的主要目的是演示一种使用RxJS开发React应用程序的替代方法。 我们希望他能给您带来深思。 有时Redux和MobX并不是必需的,但也许在这种情况下,RxJS才是适合特定项目的工具。

亲爱的读者们! 开发React应用程序时是否使用RxJS?

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


All Articles