我们使用玻璃钢泵抽React钩子

掌握了这些钩子之后,许多React开发人员都感到欣喜若狂,最终获得了一个简单便捷的工具包,使您可以用更少的代码来实现任务。 但这是否意味着我们提供了开箱即用的标准useState和useReducer钩子来管理状态?


我认为,以它们的原始形式,它们的使用不是很方便,它们更有可能被视为构建真正方便的状态管理挂钩的基础。 React开发人员自己强烈鼓励开发自定义钩子,那么为什么不这样做呢? 在切入点下,我们将看一个非常简单且易于理解的示例,普通钩子有什么问题以及如何对其进行改进,以至于它们完全拒绝使用其纯形式。


有一个用于输入的特定字段,条件是名称。 然后单击一个按钮,我们应该使用输入的名称向服务器发出请求(一定的搜索)。 似乎会更容易? 但是,解决方案远非显而易见。 第一个朴素的实现:


const App = () => { const [name, setName] = useState(''); const [request, setRequest] = useState(); const [result, setResult] = useState(); useEffect(() => { fetch('//example.api/' + name).then((data) => { setResult(data.result); }); }, [request]); return <div> <input onChange={e => setName(e.target.value)}/> <input type="submit" value="Check" onClick={() => setRequest(name)}/> { result && <div>Result: { result }</div> } </div>; } 

怎么了 如果用户在该字段中输入内容,然后发送了两次表单,则只有第一个请求对我们有效,因为 在第二次点击请求将不会更改,并且useEffect将不起作用。 如果您认为我们的应用程序是票务搜索服务,并且用户可能会不定期地一次又一次发送表单而不进行更改,那么这种实现对我们将不起作用! 使用名称作为useEffect的依赖项也是不可接受的,否则当文本更改时将立即发送表单。 好吧,你必须表现出独创性。


 const App = () => { const [name, setName] = useState(''); const [request, setRequest] = useState(); const [result, setResult] = useState(); useEffect(() => { fetch('//example.api/' + name).then((data) => { setResult(data.result); }); }, [request]); return <div> <input onChange={e => setName(e.target.value)}/> <input type="submit" value="Check" onClick={() => setRequest(!request)}/> { result && <div>Result: { result }</div> } </div>; } 

现在,每次单击时,我们将请求的含义更改为相反的含义,这将实现所需的行为。 这是一个很小且无辜的拐杖,但使代码难以理解。 也许现在看来,您似乎已经把问题从手指中吸了出来并扩大了它的规模。 好吧,要回答是否正确,您需要将此代码与其他提供更具表达力的方法的实现进行比较。


让我们在理论上使用线程抽象来看一下这个例子。 描述用户界面的状态非常方便。 因此,我们有两个流:在文本字段中输入的数据(名称为$),以及在表单的提交按钮上的单击流(单击$)。 我们需要根据它们创建到服务器的第三组合请求流。


 name$ __(C)____(Ca)_____(Car)____________________(Carl)___________ click$ ___________________________()______()________________()_____ request$ ___________________________(Car)___(Car)_____________(Carl)_ 

这是我们需要实现的行为。 每个流都具有两个方面:它具有的值以及值流经它的时间点。 在各种情况下,我们可能需要一个或另一个方面,或者两者都需要。 您可以将其与音乐中的节奏和和声进行比较。 只需要响应时间的流也称为信号。


在我们的例子中,点击$只是一个信号:哪个值流过它(未定义/ true /事件/等等),只有在这种情况发生时才重要。 案例名称$
相反:它的改变绝不意味着系统的任何改变,但是在某些时候我们可能需要它的含义。 从这两个流中,我们需要从第一个值(第二个值)取第三个值。


对于Rxjs,我们有一个现成的运算符:


 const names$ = fromEvent(...); const click$ = fromEvent(...); const request$ = click$.pipe(withLatestFrom(name$), map(([name]) => fromPromise(fetch(...)))); 

但是,Rx在React中的实际使用可能非常不便。 一个更合适的选项是mrr库,该库基于与Rx相同的功能-反应原理,但特别适合于以“总反应性”原理与React一起使用,并作为一个钩子进行连接。


 import useMrr from 'mrr/hooks'; const App = props => { const [state, set] = useMrr(props, { result: [name => fetch('//example.api/' + name).then(data => data.result), '-name', 'submit'], }); return <div> <input value={state.name} onChange={set('name')}/> <input type="submit" value="Check" onClick={set('submit')}/> { state.result && <div>Result: { state.result }</div> } </div>; } 

useMrr接口类似于useState或useReducer:它返回一个状态对象(所有线程的值)和一个setter以便将值放入线程中。 但是在内部,一切都有些不同:每个状态字段(=流),除了我们直接从DOM事件中放入值的状态字段外,都由一个函数和一个父线程列表来描述,这些更改将导致重新计算子代。 在这种情况下,父线程的值将被替换为函数。 如果我们只想获取流的值,而不是对流的更改做出响应,则可以在名称前面写一个“减号”,就像在名称的情况下一样。


从本质上讲,我们在一行中得到了所需的行为。 但这不仅仅是简洁。 让我们更详细地比较所获得的结果,首先比较诸如可读性和结果代码的清晰度之类的参数。


在mrr中,您几乎可以将“逻辑”与“模板”完全分开:您无需在JSX中编写任何复杂的命令处理程序。 一切都是极具说明性的:我们只需将DOM事件映射到相应的流,几乎无需转换(对于输入字段,将自动提取e.target.value的值,除非您另外指定),并且已经在useMrr结构中描述了基本流的形成方式子公司。 因此,在同步和异步数据转换的情况下,我们始终可以轻松跟踪我们的价值形成方式。


与Px相比:我们甚至不必使用其他运算符:如果mrr函数收到了Promise,它将自动等待直到解析并把接收到的数据放入流中。 另外,我们使用withLatestFrom代替了
被动聆听(减号),这更方便。 想象一下,除了名称,我们还需要发送其他字段。 然后在mrr中,我们将添加另一个被动侦听流:


 result: [(name, surname) => fetch(...), '-name', '-surname', 'submit'], 

并且在Rx中,您必须使用LatestFrom和地图来另外雕刻一个,或者首先将名称和姓氏组合为一个流。


但是回到钩子和先生。 始终显示数据的形成方式的更容易理解的依赖关系记录可能是主要优点之一。 当前的useEffect接口基本上不允许响应信号流,这就是为什么
我必须提出不同的想法。


另一点是,普通钩子的选项会带来额外的渲染。 如果用户只是单击了按钮,则还没有对需要绘制反应的UI进行任何更改。 但是,将调用渲染。 在具有mrr的变量中,仅当服务器的响应已经到达时,才会更新返回状态。 您节省比赛费用吗? 好吧,也许吧。 但是对我个人而言,作为“基本钩子”的基础的“在任何无法理解的情况下重新渲染自己”的原则会导致拒绝。


额外的渲染意味着事件处理程序的新形式。 顺便说一句,这里的普通钩子都是不好的。 处理程序不仅势在必行,而且每次渲染时都必须重新生成它们。 而且这里将无法完全使用缓存,因为 许多处理程序必须锁定到内部组件变量。 mrr处理程序更具声明性,并且缓存已内置于mrr中:set(“名称”)将仅生成一次,并将从缓存中替换以进行后续渲染。


随着代码库的增加,命令处理程序可能变得更加繁琐。 假设我们还需要显示用户提交的表单的数量。


 const App = () => { const [request, makeRequest] = useState(); const [name, setName] = useState(''); const [result, setResult] = useState(false); const [clicks, setClicks] = useState(0); useEffect(() => { fetch('//example.api/' + name).then((data) => { setResult(data.result); }); }, [request]); return <div> <input onChange={e => setName(e.target.value)}/> <input type="submit" value="Check" onClick={() => { makeRequest(!request); setClicks(clicks + 1); }}/><br /> Clicked: { clicks } </div>; } 

不太好看。 您当然可以将处理程序呈现为组件内部的单独函数。 可读性将提高,但是将保留每次渲染时重新生成功能的问题以及命令性问题。 从本质上讲,这是一个常规的程序代码,尽管人们普遍认为React API正在逐渐向功能方法转变。


对于那些问题规模似乎过大的人,我可以回答,例如,React的开发人员自己意识到了过多生成处理程序的问题,立即以useCallback的形式为我们提供了拐杖。


在mrr上:


 const App = props => { const [state, set] = useMrr(props, { $init: { clicks: 0, }, isValid: [name => fetch('//example.api/' + name).then(data => data.isValid), '-name', 'makeRequest'], clicks: [a => a + 1, '-clicks', 'makeRequest'], }); return <div> <input onChange={set('name')}/> <input type="submit" value="Check" onClick={set('makeRequest')}/> </div>; } 

一个更方便的替代方法是useReducer,它使您可以放弃处理程序的必要性。 但是仍然存在其他重要问题:缺乏信号处理(因为相同的useEffect将导致副作用),以及异步转换期间的可读性最差(换句话说,由于相同的useEffect,跟踪存储的字段之间的关系更加困难) ) 如果在mrr中立即清楚地看到状态字段(线程)之间的依赖关系图,则必须在钩子中上下移动一点。


同样,在同一组件中共享useState和useReducer也不是很方便(同样,会有复杂的命令式处理程序会更改useState中的某些内容)
和分派操作),因此,在开发组件之前,您很有可能需要接受一个或另一个选项。


当然,仍然可以继续考虑所有方面。 为了不超出本文的范围,我将全面介绍一些不太重要的观点。


集中记录,调试。 由于在mrr中所有流都包含在一个集线器中,因此为了进行调试,只需添加一个标志即可:


 const App = props => { const [state, set] = useMrr(props, { $log: true, $init: { clicks: 0, }, isValid: [name => fetch('//example.api/' + name).then(data => data.isValid), '-name', 'makeRequest'], clicks: [a => a + 1, '-clicks', 'makeRequest'], }); ... 

之后,所有对流的更改将显示在控制台中。 要访问整个状态(即所有线程的当前值),请使用伪流$状态:


 a: [({ name, click, result }) => { ... }, '$state', 'click'], 

因此,如果您需要或非常习惯编辑风格,则可以在mrr中以编辑器风格编写,并基于事件和整个先前状态返回新的字段值。 但是相反的方法(使用useReducer或mrr样式的编辑器编写)将无法工作,因为它们之间缺乏反应性。


随着时间的推移。 还记得流程的两个方面:含义和响应时间,和谐与节奏吗? 因此,使用第一个普通钩子是非常简单和方便的,但是使用第二个钩子-否。 通过长时间工作,我的意思是形成子流,其“节奏”与父流不同。 这主要是所有类型的过滤器,debowns,trotl等。 您很可能必须实现所有这些。 在mrr中,您可以直接使用现成的语句。 绅士设置mrr不如操作员Rx的种类多,但命名更直观。


组件间的交互。 我记得在编辑器中创建一个故事被认为是一种很好的做法。 如果我们在许多组件中使用useReducer,
各方之间的交互组织可能存在问题。 在mrr上,流可以从一个组件自由地“流动”到层次结构的上层或下层,但是由于声明性方法,这不会造成问题。 更多细节
React中的Actors + FRP文章中介绍了该主题以及mrr API的其他功能


结论


新的react钩子很棒,可以简化我们的生活,但是它们确实有一些缺陷,高级通用钩子(状态管理)可以解决。 提出并考虑了功能反应性mrr库中的UseMrr。


问题及其解决方案:


  • 每次渲染时不必要的数据重新计数(由于基于推送的反应性,因此缺少mrr)
  • 状态更改不要求UI更改时的额外渲染
  • 异步转换(与同步转换相比)的代码可读性较差。 在mrr中,异步代码在可读性和表达性上不逊于同步。 在最近一篇有关useEffect on mrr的文章中讨论的大多数问题基本上是不可能的
  • 并非总是可缓存的命令式处理程序(在mrr中,它们是自动缓存的,几乎总是可以缓存的,是声明式的)
  • 同时使用useState和useReducer可以创建尴尬的代码
  • 缺乏随时间转换流量的工具(防抖,油门,比赛条件)

在许多方面,人们可以争辩说可以通过自定义钩子解决它们。 但是,这恰恰是所提议的内容,而不是针对不同的实现,对于每个单独的任务,提出了一个整体,一致的解决方案。


许多问题已经变得太熟悉了,我们无法清楚地认识到。 例如,异步转换总是比同步转换看起来更复杂和混乱,从这个意义上讲,钩子并不比早期的方法(eds等)更糟糕。 要意识到这是一个问题,您必须首先查看提供更好解决方案的其他方法。


本文的目的不是强加任何特定的观点,而是要提请注意该问题。 我确信存在或正在创建其他解决方案,这些解决方案可以成为值得的替代方案,但尚未广为人知。 即将推出的React Cache API也可以发挥很大的作用。 我将很高兴在评论中进行批评和讨论。


感兴趣的人还可以在3月28日在kyivjs上观看有关此主题的演示。

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


All Articles