我叫Artyom Berezin,我是Yandex几种内部服务的开发人员。 在过去的六个月中,我一直在与React Hooks积极合作。 在此过程中,必须克服一些困难。 现在,我想与您分享这种经验。 在报告中,我从实际的角度检查了React Hook API-为什么我们需要钩子,切换是否值得,在移植时最好考虑一下 在转换过程中容易犯错误,但是避免错误也不是那么困难。

-挂钩只是描述组件逻辑的另一种方法。 它使您可以向功能组件添加某些以前仅是类中组件固有的功能。

首先,它支持内部状态,然后-支持副作用。 例如-网络请求或对WebSocket的请求:订阅,某些渠道的取消订阅。 或者,也许我们正在谈论对其他一些异步或同步浏览器API的请求。 同样,钩子使我们能够访问组件的生命周期,生命的开始(即安装),更新其道具以及死亡。

可能是比较中最简单的说明方式。 这是只能与类中的组件一起使用的最简单的代码。 该组件正在改变某些东西。 这是一个可以增加或减少的常规计数器,状态仅为一个字段。 总的来说,我认为如果您熟悉React,那么代码对您来说是完全显而易见的。

一个执行完全相同功能但用钩子编写的相似组件看起来要紧凑得多。 根据我的计算,平均而言,当从类上的组件移植到钩子上的组件时,代码减少了大约一半半,并且很令人满意。
关于钩子如何工作的几句话。 钩子是在React内部声明的全局函数,每次渲染组件时都会被调用。 React跟踪对这些函数的调用,可以更改其行为或决定应返回什么。

在使用钩子方面存在一些限制,这些钩子将它们与普通功能区分开。 首先,它们不能在类的组件中使用,只是这样的限制适用,因为它们不是为它们创建的,而是为功能组件创建的。 不能在内部函数,循环,条件内部调用钩子。 仅在组件功能内部的嵌套的第一层。 此限制由React本身施加,以便能够跟踪调用了哪些钩子。 他按一定顺序将它们堆叠在大脑中。 然后,如果此命令突然更改或消失,则可能会出现复杂,难以捉摸,难以调试的错误。
但是,如果您的逻辑很复杂,并且想在钩子内部使用钩子,那么很可能表明您应该制作一个钩子。 假设您在一个单独的自定义钩子中创建了几个相互连接的钩子。 在其中,您可以使用其他自定义钩子,从而构建钩子的层次结构,突出显示那里的常规逻辑。

钩子提供了一些优于类的优点。 首先,如前所述,使用自定义钩子,您可以更容易地弄乱逻辑。 以前,使用带有高阶组件的方法,我们设计了某种共享逻辑,它是组件的包装。 现在,我们将此逻辑放在钩子中。 因此,减少了组件树:减少了其嵌套,并且React可以更轻松地跟踪组件更改,重新计算树,重新计算虚拟DOM等。这解决了所谓的wrapper-hell问题。 我认为,那些与Redux合作的人对此很熟悉。
使用钩子编写的代码更容易通过现代最小化器(例如Terser或旧的UglifyJS)最小化。 事实是,我们不需要保存方法的名称,我们不需要考虑原型。 编译后,如果目标是ES3或ES5,我们通常会得到一堆修补的原型。 这里不需要做所有这些事情,因此更容易将其最小化。 而且,由于不使用类,因此我们无需考虑这一点。 对于初学者来说,这通常是一个大问题,并且可能是导致Bug的主要原因之一:我们忘记了这可能是一个窗口,我们需要例如在构造函数中或以其他方式绑定方法。
另外,使用挂钩可以使您突出显示控制任何一种副作用的逻辑。 以前,这种逻辑(尤其是当我们对某个组件有多个副作用时)必须分为不同的组件生命周期方法。 而且,由于出现了最小化挂钩,因此React.memo出现了,现在功能组件可以进行记忆化,也就是说,如果其组件未更改,则不会与我们一起重新创建或更新此组件。 以前不可能做到这一点,现在有可能。 所有功能组件都可以包装在备忘录中。 还在useMemo挂钩中出现了,我们可以使用它来计算一些重值,或者仅实例化一些实用程序类。
如果我不谈论一些基本的问题,报告将是不完整的。 首先,这些是状态管理挂钩。

首先-useState。

一个示例与报告开头的示例类似。 useState是一个具有初始值的函数,并从当前值和该函数返回一个元组来更改该值。 所有魔术都是由React内部提供的。 我们可以简单地读取或更改该值。
与类不同,我们可以根据需要使用任意数量的状态对象,将状态分为逻辑部分,以免像类中那样将它们混合在一个对象中。 这些部分将彼此完全隔离:它们可以彼此独立地进行更改。 例如,该代码的结果:我们更改两个变量,计算结果并显示按钮,这些按钮使我们可以在此更改第一个变量,在此更改第二个变量。 记住这个例子,因为稍后我们将做类似的事情,但是要复杂得多。

Redux爱好者在类固醇上有一个useState。 它允许您使用化简器更一致地更改状态。 我认为那些熟悉Redux的人甚至无法解释,对于那些不熟悉的人,我会告诉他们。
约简器是一种接受状态和某些对象的函数,通常称为操作,描述该状态应如何改变。 更确切地说,它传递一些参数,并且在减速器内部,它已经根据它们的参数确定状态的改变方式,因此,应该返回并更新新的状态。

大致以此方式在组件代码中使用它。 我们有一个useReducer钩子,它带有一个reducer函数,第二个参数是状态的初始值。 返回诸如useState,当前状态和要对其进行更改的函数,如dispatch。 如果将动作对象传递给调度对象,我们将调用状态更改。

非常重要的用途是效果钩子。 它使您可以向组件添加副作用,从而为生命周期提供替代方案。 在此示例中,我们使用一个具有useEffect的简单方法:它只是例如使用API向服务器请求一些数据,然后在页面上显示这些数据。

UseEffect有一个高级模式,这是当传递给useEffect的函数返回某个其他函数时,则在应用useEffect的下一个循环中将调用此函数。
我忘了提一下,在将更改应用于DOM之后,useEffect被异步调用。 也就是说,它保证将在渲染组件之后执行它,并且如果某些值发生更改,则可以导致下一次渲染。

在这里,我们第一次遇到了依赖的概念。 一些挂钩-useEffect,useCallback,useMemo-将值的数组作为第二个参数,这将使我们能够说出要跟踪的内容。 此数组的更改会导致某种影响。 例如,假设在这里,我们具有某种组件,用于从某些列表中选择作者。 还有一本作者写的书。 当作者更改时,将调用useEffect。 更改了authorId后,将调用一个请求并加载书籍。
我还提到了传递诸如useRef的钩子,这是React.createRef的替代方法,类似于useState,但对ref的更改不会导致渲染。 有时对于某些黑客来说很方便。 useImperativeHandle允许我们在组件上声明某些“公共方法”。 如果在父组件中使用useRef,则它可以拉出这些方法。 老实说,我曾经出于教育目的尝试过一次,但实际上它没有用。 useContext只是一件好事,如果提供者在层次结构级别中更高的位置定义了此值,则它允许您从上下文中获取当前值。
有一种优化挂钩上的React应用程序的方法;这就是备忘录。 记忆可分为内部和外部。 首先关于外面。

这是React.memo,实际上是React.PureComponent类的替代方法,该类仅在prop或状态更改时才跟踪prop和更改的组件的更改。
但是,在这里,类似的东西没有状态。 它还监视道具的更改,如果道具已更改,则会发生渲染器。 如果道具没有更改,则组件不会更新,因此我们保存了这一点。

内部优化方法。 首先,这是一个相当底层的东西-useMemo,很少使用。 它允许您计算一些值,并仅在依赖项中指定的值发生更改时重新计算它。

useMemo的特殊情况是一个称为useCallback的函数。 它主要用于记住将传递给子组件的事件处理程序函数的值,以使这些子组件无法再次呈现。 它使用简单。 我们描述一个特定的函数,将其包装在useCallback中,并指出它所依赖的变量。
许多人有一个问题,但是我们需要这个吗? 我们需要钩子吗? 我们是像以前一样搬家还是住? 没有一个答案,这完全取决于首选项。 首先,如果您直接与面向对象的编程紧密联系在一起,那么如果您的组件作为一个类来使用,它们具有可以拉出的方法,那么可能对您来说这件事似乎是多余的。 原则上,当我第一次听说钩子时,在我看来似乎太复杂了,正在添加某种魔术,并且不清楚原因。
对于功能迷来说,这是必须具备的,因为钩子是函数,而函数编程技术也适用于它们。 例如,您可以使用Ramda之类的库将它们组合起来或做任何事情。

由于我们摆脱了类,因此不再需要将此上下文绑定到方法。 如果使用这些方法作为回调。 通常,这是一个问题,因为您必须记住将它们绑定在构造函数中,或者使用语言语法的非官方扩展名,例如属性的箭头功能。 相当普遍的做法。 我使用了我的装饰器,从原理上讲,这也是实验方法。

生命周期的工作方式和管理方式有所不同。 挂钩将几乎所有生命周期操作与useEffect挂钩相关联,该挂钩使您可以订阅组件的诞生和更新以及组件的死亡。 为此,在类中,我们必须重新定义几种方法,例如componentDidMount,componentDidUpdate和componentWillUnmount。 而且,应该可以用React.memo替换shouldComponentUpdate方法。

状态的处理方式相差很小。 首先,类具有一个状态对象。 我们必须在那里塞满任何东西。 在钩子中,我们可以将逻辑状态分成几部分,这对于我们分开进行操作很方便。
类上组件的setState()允许指定状态补丁,从而更改状态的一个或多个字段。 在钩子中,我们必须整体改变整个状态,这甚至是一件好事,因为使用各种不可变的事物很流行,而且永远不要期望我们的对象发生变异。 他们对我们来说总是新的。
钩子没有的类的主要特征是:我们可以订阅状态更改。 也就是说,我们更改状态,并立即订阅其更改,在应用更改后必须立即处理某些内容。 用钩子,这是行不通的。 这需要以一种非常有趣的方式完成,我将进一步告诉您。
还有一些有关功能更新的方法。 当状态更改功能接受另一个功能时,该状态不应该更改,而应该创建,因此它可以在那里工作。 如果使用类组件,它可以向我们返回某种补丁,那么在挂钩中,我们必须返回整个新值。
通常,无论是否移动,您都不太可能获得答案。 但是我建议至少尝试尝试一下,至少对于新代码而言。 当我刚开始使用钩子时,我立即确定了几个自定义钩子,这些钩子对我的项目很方便。 基本上,我试图替换通过高阶组件实现的某些功能。

useDismount-对于熟悉RxJS的人,有机会通过向每个Observable订阅特殊对象Subject来大规模取消订阅单个组件或单个函数中的所有Observable,并在关闭时取消所有订阅。 如果组件很复杂,这很方便,如果Observable内部有许多异步操作,那么一次取消订阅(而不是单独订阅)是很方便的。
当其中出现一个新值时,useObservable从Observable返回一个值。 类似的useBehaviourSubject钩子从BehaviourSubject返回。 它与Observable的区别在于它最初具有一定的含义。
方便的自定义钩子useDebouncedValue允许我们组织例如搜索字符串的主题,这样,并非每次您按下键时都向服务器发送内容,而是等到用户完成键入操作。
两个类似的钩子。 useWindowResize返回窗口大小的当前实际值。 滚动位置的下一个挂钩是useWindowScroll。 如果有CSS不能完成的任何复杂操作,我将使用它们来重新计算一些弹出窗口或模式窗口。
还有一个用于实现热键的小钩子,当该组件出现在页面上时,它就被订阅了某个热键。 他去世时,会自动取消订阅。
这些自定义挂钩有什么用处? 我们可以在钩子中塞入一个取消订阅的控件,而不必考虑手动取消使用该钩子的组件中的某个位置的订阅。
不久前,他们向我扔了一个指向react-use库的链接,事实证明,大多数自定义钩子已经在其中实现了。 我写了一辆自行车。 有时这很有用,但是将来,我很可能会把它们扔掉,并进行反应使用。 而且我建议您也查看是否打算使用挂钩。

实际上,该报告的主要目标是显示如何错误书写,可能出现的问题以及如何避免这些问题。 首先,可能是任何人在研究这些挂钩并尝试编写某些东西时,都会错误地使用useEffect。 这是与所有人尝试钩子时100%编写的代码相似的代码。 这是由于useEffect最初是在心理上被感知的,它是componentDidMount的替代方案。 但是,与componentDidMount(仅调用一次)不同,useEffect在每个渲染器上调用。 而且这里的错误是它更改了数据变量,并且同时更改它导致了组件渲染器,结果将重新请求效果。 因此,我们收到了对服务器的无休止的一系列AJAX请求,并且组件本身不断更新,更新,更新。

修复它非常简单。 您需要在此处添加一个空数组,其中包含它所依赖的那些依赖项,并且其中的更改将重新启动效果。 如果我们在此处指定了一个空的依赖关系列表,那么相应的效果将不会重新启动。 这不是某种技巧,它是使用useEffect的基本功能。

假设我们已修复它。 现在有点复杂。 我们有一个组件,用于呈现某些需要从服务器获取的某种ID。 在这种情况下,原则上一切正常,直到我们更改父级中的entityId,也许这与您的组件无关。

但是最有可能的是,如果它发生更改或需要更改它,并且页面上有一个旧组件并且事实证明它没有更新,那么最好在此处添加entityId作为依赖项,从而导致更新,更新数据。

使用useCallback的更复杂的示例。 乍一看,这里一切都很好。 我们有一个页面上有某种倒数计时器,或者相反,只是一个滴答作响的计时器。 并且,例如,主机列表,最上方是过滤器,允许您过滤此主机列表。 好了,此处添加了维护,只是为了说明转换为渲染器的频繁更改的值。
, , maintenance , , , onChange. onChange, . , HostFilters - , , dropdown, . , . , .

onChange useCallback. , .
, . , , . Facebook, React. , , , , '. , , confusing .

? — , - , , , , , . .
, , , , , , . , Garbage Collector , . , , , , . , , , reducer, , . , .
, , . - , , setValue - , , setState . - useEffect.
useEffect - , - , , , useEffect. useEffect , . , , Backbone, : , , , - . , , - , . - . , , , , - . , , , , , , . .
, , . , , . , . , . , , , dropdown . , . dropdown pop-up, useWindowScroll, useWindowResize , . , , — , .
, , . , , , , , . , , , , , .

, «», . , , TypeScript . . , reducer Redux , action. , action , action. , , , .
. , action. , , IncrementA 0, 1, 2, . . , , , , . action action, - . UnionType “Action”, , , action. .
— . , initialState, . , - . TypeScript. . , typeState , initialState.

reducer. State, Action, : switch action.type. TypeScript UnionType: case, - , type. action .
, : , , . .

? , . . , reducer. , action creator , , dispatch.

extension Dev Tools. . .
, , . , , . useDebugValue , - Dev Tool. useConstants, - , loaded, , , .

— . , . , . , , , . , , — - , — .
. Facebook ESLint, . , , . , dependencies . , , , .
, , , - , . , , , . . , - - .
— , , - . , , . , , - . , . . :
- «React hooks — ?» C . , , , , .
- useEffect . , , , , , . .
- «useReducer vs useState in React» , useReducer, useState. : , , , useReducer. - , useState .
- React Hooks CheatSheets c .
- . Usehooks.com — , . . react-use — , , .