用Observable和React Hooks替换Redux

状态管理是React开发中解决的最重要的任务之一。 已经创建了许多工具来帮助开发人员解决此问题。 最受欢迎的工具是Redux,这是由Dan Abramov创建的一个小型库,可帮助开发人员在其应用程序中使用Flux设计模式。 在本文中,我们将了解我们是否真的需要Redux,并了解如何用基于Observable和React Hooks的更简单方法替换它。


为什么我们完全需要Redux?


Redux与React经常联系在一起,以至于许多开发人员在没有考虑为什么需要Redux的情况下使用它。 通过React,可以轻松地将组件及其状态与setState() / useState()进行同步。 但是,一旦状态被多个组件同时使用,一切就会变得更加复杂。 在多个组件之间共享公共状态的最明显解决方案是将其(状态)移动到其公共父级。 但是,这种“直接”解决方案会很快导致困难:如果组件在组件层次结构中彼此远离,则更新一般状态将需要大量滚动组件的属性。 React Context可以帮助减少溢出的次数,但是每次状态开始与另一个组件一起使用时声明一个新的上下文将需要更多的努力,并最终可能导致错误。


Redux通过引入一个Store对象解决了这些问题,该对象包含应用程序的整个状态。 在需要访问状态的组件中,此Store是使用connect函数实现的。 此功能还确保状态改变时,依赖于该状态的所有组件都将被重绘。 最后,为了更改状态,组件必须发送触发reducer的操作以计算新的更改状态。



当我第一次了解Redux的概念时


Redux有什么问题?


第一次阅读正式的Redux教程时 ,我为更改状态而不得不编写大量代码而感到震惊。 更改状态需要声明一个新动作,实现相应的reducer,最后发送该动作。 Redux还鼓励编写动作创建者 ,使您每次提交动作时都可以轻松创建动作。


通过所有这些步骤,Redux使代码理解,重构和调试变得复杂。 阅读其他人编写的代码时,通常很难弄清楚提交动作后会发生什么。 首先,我们必须深入研究动作创建者代码,以找到适当的动作类型,然后找到处理此类动作的减速器。 如果使用某些中间件,例如redux-saga,事情可能会变得更加复杂,这使得解决方案上下文更加隐含。


最后,当使用TypeScript时,Redux可能令人失望。 按照设计,动作只是与其他参数关联的字符串。 有多种方法可以使用TypeScript编写类型良好的Redux代码,但它可能非常繁琐,并且可能导致我们不得不编写的代码数量增加。



用Redux编写的学习代码的快感


可观察和钩住:一种管理状态的简单方法。


用可观察的替换商店


为了以一种更简单的方式解决状态共享的问题,我们首先需要找到一种在其他组件更改其一般状态时通知组件的方法。 为此,我们创建一个Observable类,该类包含一个值,并允许组件预订对该值的更改。 subscription方法将返回为了从Observable退订必须调用的函数。


在TypeScript上,实现这样的类非常简单:


 type Listener<T> = (val: T) => void; type Unsubscriber = () => void; export class Observable<T> { private _listeners: Listener<T>[]; constructor(private _val: T) {} get(): T { return this._val; } set(val: T) { if (this._val !== val) { this._val = val; this._listeners.forEach(l => l(val)); } } subscribe(listener: Listener<T>): Unsubscriber { this._listeners.push(listener); return () => { this._listeners = this._listeners.filter(l => l !== listener); }; } } 

如果将此类与Redux Store进行比较,您会发现它们非常相似: get()对应于getState()subscribe()相同。 主要区别是dispatch()方法,已由更简单的set()方法代替,该方法使您无需依赖reducer即可更改其中包含的值。 与Redux相比,另一个重要的区别是,我们将使用大量的Observable而不是包含所有状态的单个Store


更换减速机服务


现在可以将Observable用来存储一般状态,但是我们仍然需要移动化简器中包含的逻辑。 为此,我们使用服务的概念。 服务是实现我们应用程序的整个业务逻辑的类。 让我们尝试使用Observable将Reduce教程中的reducer Todo重写为Todo服务:


 import { Observable } from "./observable"; export interface Todo { readonly text: string; readonly completed: boolean; } export enum VisibilityFilter { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE, } export class TodoService { readonly todos = new Observable<Todo[]>([]); readonly visibilityFilter = new Observable(VisibilityFilter.SHOW_ALL); addTodo(text: string) { this.todos.set([...this.todos.get(), { text, completed: false }]); } toggleTodo(index: number) { this.todos.set(this.todos.get().map( (todo, i) => (i === index ? { text: todo.text, completed: !todo.completed } : todo) )); } setVisibilityFilter(filter: VisibilityFilter) { this.visibilityFilter.set(filter); } } 

与reducer Todo进行比较,我们可以注意到以下差异:


  • 动作被方法替换,从而无需声明动作类型,动作本身和动作创建者。
  • 您不再需要编写大型开关来在操作类型之间进行路由。 动态Javascript分配(即方法调用)可以解决此问题。
  • 最重要的是,服务包含并更改其管理的状态。 与纯函数的化简器相比,这是一个很大的概念差异。

访问服务并可以从组件中观察


现在,我们已将“来自Redux的存储和缩减器”替换为“可观察和服务”,我们需要使所有React组件都可以使用这些服务。 有几种方法可以做到这一点:我们可以使用IoC框架,例如Inversify; 使用React上下文或使用与Store Redux中相同的方法-每个服务一个全局实例。 在本文中,我们将考虑最后一种方法:


 import { TodoService } from "./todoService"; export const todoService = new TodoService(); 

现在,我们可以通过导入todoService实例来访问共享状态并从所有React组件中更改它。 但是,当一般状态被另一个组件更改时,我们仍然需要找到一种方法来重绘我们的组件。 为此,我们将编写一个简单的钩子,该钩子将状态变量添加到组件,订阅Observable并在Observable值更改时更新状态变量:


 import { useEffect, useState } from "react"; import { Observable } from "./observable"; export function useObservable<T>(observable: Observable<T>): T { const [val, setVal] = useState(observable.get()); useEffect(() => { setVal(observable.get()); //   @mayorovp return observable.subscribe(setVal); }, [observable]); return val; } 

全部放在一起


我们的工具包已准备就绪。 我们可以使用Observable在服务中存储一般状态,并使用useObservable来确保组件始终与此状态保持同步。


让我们使用新的钩子从Redux教程重写TodoList组件:


 import React from "react"; import { useObservable } from "./observableHook"; import { todoService } from "./services"; import { Todo, VisibilityFilter } from "./todoService"; export const TodoList = () => { const todos = useObservable(todoService.todos); const filter = useObservable(todoService.visibilityFilter); const visibleTodos = getVisibleTodos(todos, filter); return ( <div> <ul> {visibleTodos.map((todo, index) => ( <TodoItem key={index} todo={todo} index={index} /> ))} </ul> <p> Show: <FilterLink filter={VisibilityFilter.SHOW_ALL}>All</FilterLink>, <FilterLink filter={VisibilityFilter.SHOW_ACTIVE}>Active</FilterLink>, <FilterLink filter={VisibilityFilter.SHOW_ALL}>Completed</FilterLink> </p> </div> ); }; const TodoItem = ({ todo: { text, completed }, index }: { todo: Todo; index: number }) => { return ( <li style={{ textDecoration: completed ? "line-through" : "none", }} onClick={() => todoService.toggleTodo(index)} > {text} </li> ); }; const FilterLink = ({ filter, children }: { filter: VisibilityFilter; children: React.ReactNode }) => { const activeFilter = useObservable(todoService.visibilityFilter); const active = filter === activeFilter; return active ? ( <span>{children}</span> ) : ( <a href="" onClick={() => todoService.setVisibilityFilter(filter)}> {children} </a> ); }; function getVisibleTodos(todos: Todo[], filter: VisibilityFilter): Todo[] { switch (filter) { case VisibilityFilter.SHOW_ALL: return todos; case VisibilityFilter.SHOW_COMPLETED: return todos.filter(t => t.completed); case VisibilityFilter.SHOW_ACTIVE: return todos.filter(t => !t.completed); } } 

如我们所见,我们已经编写了一些引用常规状态值的组件( todostodos )。 只需通过从todoService调用方法即可更改这些值。 由于钩子useObservable可以订阅值更改,因此在常规状态更改时会自动重绘这些组件。


结论


如果将此代码与Redux方法进行比较,我们将看到几个优点:


  • 简洁:我们唯一要做的就是将状态值包装在Observable并在从组件访问这些值时使用钩子useObservable 。 无需声明动作,动作创建者,编写或组合reducer或使用mapStateToPropsmapDispatchToProps将我们的组件连接到存储库。
  • 简便性:现在,跟踪代码执行更加容易。 了解按下按钮时实际发生的事情只是切换到被调用方法的实现。 使用调试器的逐步执行也得到了显着改善,因为我们的组件和服务之间没有中间层。
  • 开箱即用的类型安全性: TypeScript开发人员不需要其他工作即可正确键入代码。 无需为状态和每个动作声明类型。
  • 支持异步/等待:尽管此处未进行演示,但此解决方案可与异步功能配合使用,极大地简化了异步编程。 无需依赖中间件,例如redux-thunk,这需要对功能编程的深入了解才能理解。

当然,Redux仍然具有一些重要的优势,特别是Redux DevTools,它使开发人员可以监视开发期间的状态变化并及时移至过去的应用程序状态,这可以是调试的绝佳工具。 但是根据我的经验,我很少使用它,付出的代价似乎太高了,以至于无法获得小幅收益。


在我们所有的React和React Native应用程序中,我们都成功地使用了与本文所述类似的方法。 实际上,我们从来没有觉得需要比这更复杂的状态管理系统。


注意事项


这篇文章中介绍的Observable类非常简单。 可以用更高级的实现方式来代替它,例如微观察者(我们自己的库)或RxJS。


这里介绍的解决方案与MobX可以实现的解决方案非常相似。 主要区别在于MobX支持对象状态的深层可变性。 当事情无法按预期进行时,它还依靠ES6代理来通知更改,隐式重新渲染以及使调试复杂化。 此外,MobX不能与异步功能很好地配合使用。


从翻译者那里:该出版物可以看作是使用Observable进行状态管理的简介,是对用RxJS / Immer管理应用程序状态作为Redux / MobX的简单替代品中的主题的延续,该文章描述了如何简化这种方法的使用。

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


All Articles