Revolution or pain? Yandex React Hooks Report

My name is Artyom Berezin, I am a developer of several Yandex internal services. For the past six months, I have been actively working with React Hooks. In the process, there were some difficulties that had to be fought. Now I want to share this experience with you. In the report, I examined the React Hook API from a practical point of view - why do we need hooks, is it worth it to switch, which is better to consider when porting. It’s easy to make mistakes during the transition, but avoiding them is also not so difficult.

- Hooks are just another way to describe the logic of your components. It allows you to add to the functional components some features that were previously inherent only to components on classes.

First of all, it is support for the internal state, then - support for side effects. For example - network requests or requests to WebSocket: subscription, unsubscription from some channels. Or, perhaps, we are talking about requests to some other asynchronous or synchronous browser APIs. Also, hooks give us access to the component’s life cycle, to its beginning of life, that is, mounting, to updating its props and to its death.

Probably the easiest way to illustrate in comparison. Here is the simplest code that can only be with a component in classes. The component is changing something. This is a regular counter that can be increased or decreased, just one field in state. In general, I think that if you are familiar with React, the code is completely obvious to you.

A similar component that performs exactly the same function, but written in hooks, looks much more compact. According to my calculations, on average, when porting from components on classes to components on hooks, the code decreases about one and a half times, and it pleases.

A few words about how hooks work. A hook is a global function that is declared inside React and is called every time a component is rendered. React tracks the calls to these functions and can change its behavior or decide what it should return.

There are some restrictions on the use of hooks that distinguish them from ordinary functions. First of all, they cannot be used in components on classes, just such a restriction applies, because they are created not for them, but for functional components. Hooks cannot be called inside internal functions, inside loops, conditions. Only at the first level of nesting, inside the component functions. This restriction is imposed by React itself in order to be able to track which hooks were called. And he stacks them in a certain order in his brain. Then, if this order suddenly changes or some disappears, complex, elusive, difficult to debug errors are possible.

But if you have rather complicated logic and would like to use, for example, hooks inside hooks, then most likely this is a sign that you should make a hook. Suppose you make several hooks connected to each other in a separate custom hook. And inside it you can use other custom hooks, thereby building a hierarchy of hooks, highlighting the general logic there.

Hooks provide some advantages over classes. First of all, as follows from the previous one, using custom hooks, you can fumble logic much easier. Previously, using the approach with Higher Order Components, we laid out some kind of shared logic, and it was a wrapper over the component. Now we put this logic inside the hooks. Thus, the component tree is reduced: its nesting is reduced, and it becomes easier for React to track component changes, recalculate the tree, recalculate the virtual DOM, etc. This solves the problem of the so-called wrapper-hell. Those who work with Redux, I think, are familiar with this.

Code written using hooks is much easier to minimize with modern minimizers like Terser or old UglifyJS. The fact is that we do not need to save the names of methods, we do not need to think about prototypes. After transpilation, if target is ES3 or ES5, we usually get a bunch of prototypes that patches. Here all this does not need to be done, therefore it is easier to minimize. And, as a result of not using classes, we do not need to think about this. For beginners, this is often a big problem and probably one of the main reasons for bugs: we forget that this may be a window, that we need to bind the method, for example, in the constructor or in some other way.

Also, the use of hooks allows you to highlight the logic that controls any one side effect. Previously, this logic, especially when we have several side effects for a component, had to be divided into different methods of the component’s life cycle. And, since minimization hooks appeared, React.memo appeared, now the functional components lend themselves to memoization, that is, this component will not be recreated or updated with us if its props have not changed. This could not be done before, now it is possible. All functional components can be wrapped in memo. Also inside the useMemo hook appeared, which we can use to calculate some heavy values, or instantiate some utility classes only once.

The report will be incomplete if I do not talk about some basic hooks. First of all, these are state management hooks.

First of all - useState.

An example is similar to the one at the beginning of the report. useState is a function that takes an initial value, and returns a tuple from the current value and the function to change that value. All magic is served by React internally. We can simply either read this value or change it.

Unlike classes, we can use as many state objects as we need, breaks the state into logical pieces so as not to mix them in one object, as in classes. And these pieces will be completely isolated from each other: they can be changed independently of each other. The result, for example, of this code: we change two variables, calculate the result and display buttons that allow us to change the first variable to and fro, and the second variable to and fro. Remember this example, because later on we will do a similar thing, but much more complicated.

There is such a useState on steroids for Redux lovers. It allows you to more consistently change the state using a reducer. I think that those who are familiar with Redux can not even explain, for those who are unfamiliar, I will tell.

A reducer is a function that takes a state, and an object, usually called action, that describes how this state should change. More precisely, it passes some parameters, and inside the reducer it already decides, depending on their parameters, how the state will change, and as a result, a new state should be returned, updated.

In approximately this way it is used in the component code. We have a useReducer hook, it takes a reducer function, and the second parameter is the initial value of the state. Returns, like useState, the current state, and the function to change it is dispatch. If you pass an action object to dispatch, we will invoke a state change.

Very important useEffect hook. It allows you to add side effects to the component, giving an alternative to the life cycle. In this example, we use a simple method with useEffect: it is just requesting some data from the server, with the API, for example, and displaying this data on the page.

UseEffect has an advanced mode, this is when the function passed to useEffect returns some other function, then this function will be called in the next cycle, when this useEffect will be applied.

I forgot to mention, useEffect is called asynchronously, right after the change is applied to the DOM. That is, it guarantees that it will be executed after the component is rendered, and can lead to the next render if some values ​​change.

Here we meet for the first time with such a concept as dependencies. Some hooks - useEffect, useCallback, useMemo - take an array of values ​​as the second argument, which will allow us to say what to track. Changes in this array lead to some kind of effects. For example, here, hypothetically, we have some kind of component for choosing an author from a list. And a plate with books by this author. And when the author changes, useEffect will be called. When this authorId is changed, a request will be called and books will be loaded.

I also mention in passing hooks such as useRef, this is an alternative to React.createRef, something similar to useState, but changes to ref do not lead to rendering. Sometimes convenient for some hacks. useImperativeHandle allows us to declare certain “public methods” on the component. If you use useRef in the parent component, then it can pull these methods. To be honest, I tried it once for educational purposes, in practice it was not useful. useContext is a really good thing, it allows you to take the current value from the context if the provider has defined this value somewhere higher in the hierarchy level.

There is one way to optimize React applications on hooks; this is memoization. Memoization can be divided into internal and external. First about the outside.

This is React.memo, practically an alternative to the React.PureComponent class, which tracked changes in props and changed components only when props or state changed.

Here a similar thing, though without a state. It also monitors changes in props, and if the props have changed, a renderer occurs. If the props have not changed, the component is not updated, and we save on this.

Internal methods of optimization. First of all, this is a rather low-level thing - useMemo, rarely used. It allows you to calculate some value, and recalculate it only if the values ​​specified in the dependencies have changed.

There is a special case of useMemo for a function called useCallback. It is primarily used to memoize the value of event handler functions that will be passed to child components so that these child components can not be rendered again. It is used simply. We describe a certain function, wrap it in useCallback, and indicate which variables it depends on.

Many people have a question, but do we need this? Do we need hooks? Are we moving or staying as before? There is no single answer, it all depends on preferences. First of all, if you are directly rigidly tied to object-oriented programming, if your components, you are used to being a class, they have methods that can be pulled, then, probably, this thing may seem superfluous to you. In principle, it seemed to me when I first heard about hooks that it was too somehow overcomplicated, some kind of magic was being added, and it was not clear why.

For lovers of functionalities, this is, let's say, a must have, because hooks are functions, and functional programming techniques are applicable to them. For example, they can be combined or generally done anything, using, for example, libraries such as Ramda, and the like.

Since we got rid of classes, we no longer need to bind this context to methods. If you use these methods as callbacks. Usually, this was a problem, because you had to remember to bind them in the constructor, or use an unofficial extension of the language syntax, such arrow-functions as a property. Pretty common practice. I used my decorator, which is also, in principle, experimentally, on methods.

There is a difference in how the life cycle works, how to manage it. Hooks associate almost all life cycle actions with the useEffect hook, which allows you to subscribe to both the birth and update of a component and its death. In classes, for this, we had to redefine several methods, such as componentDidMount, componentDidUpdate, and componentWillUnmount. Also, the shouldComponentUpdate method can now be replaced with React.memo.

There is a fairly small difference in how the state is handled. First, classes have one state object. We had to cram anything there. In hooks, we can break the logical state into some pieces, which it would be convenient for us to operate separately.

setState () on components on classes allowed to specify a state patch, thereby changing one or more fields of the state. In hooks, we have to change the whole state as a whole, and it’s even good, because it’s fashionable to use all sorts of Immutable things and never expect our objects to mutate. They are always new with us.

The main feature of classes that hooks do not have: we could subscribe to state changes. That is, we change the state, and immediately subscribe to its changes, imperatively processing something immediately after the changes are applied. In hooks, this just doesn’t work. This needs to be done in a very interesting way, I will tell you further.

And a little about the functional way of updating. It works both there and there, when the state change functions accept another function, which this state should not change, but rather create. And if in the case of the class component it can return some kind of patch to us, then in the hooks we must return the whole new value.

In general, you are unlikely to get an answer whether to move or not. But I advise at least to try, at least for the new code, to feel it. When I just started working with hooks, I immediately identified several custom hooks that are convenient for myself for my project. Basically, I tried to replace some of the features that I had implemented through Higher Order Components.

useDismounted - for those who are familiar with RxJS, there is the opportunity to unsubscribe in bulk from all Observable within one component, or within one function, by subscribing each Observable to a special object, Subject, and when it is closed, all subscriptions are canceled. This is very convenient if the component is complex, if there are many asynchronous operations inside the Observable, it is convenient to unsubscribe from all at once, and not from each separately.

useObservable returns a value from Observable when a new one appears there. A similar useBehaviourSubject hook returns from BehaviourSubject. Its difference from Observable is that it initially has some meaning.

The convenient custom hook useDebouncedValue allows us to organize, for example, a sujest for the search string, so that not every time you press a key, send something to the server, but wait until the user finishes typing.

Two similar hooks. useWindowResize returns current actual values ​​for window sizes. The next hook for the scroll position is useWindowScroll. I use them to recount some pop-ups or modal windows, if there are some complicated things that just can't be done with CSS.

And such a small hook for implementing hot keys, which the component, when it is present on the page, it is subscribed to some hot key. When he dies, an automatic unsubscribe occurs.

What are these custom hooks convenient for? That we can cram an unsubscribe inside a hook, and we don’t have to think about manually unsubscribing somewhere in the component where this hook is used.

Not so long ago they threw me a link to the react-use library, and it turned out that most of these custom hooks were already implemented there. And I wrote a bike. This is sometimes useful, but in the future, most likely, I will probably throw them out and use react-use. And I advise you to also see if you intend to use hooks.

Actually, the main goal of the report is to show how to write incorrectly, what problems can be and how to avoid them. The very first thing, probably what anyone who is studying these hooks and trying to write something, is to use useEffect incorrectly. Here is the code similar to which 100% everyone wrote if they tried hooks. It is due to the fact that useEffect is initially perceived mentally as an alternative to componentDidMount. But, unlike componentDidMount, which is called only once, useEffect is called on every render. And the error here is that it changes, say, the data variable, and at the same time changing it leads to a component renderer, as a result, the effect will be re-requested. Thus, we get an endless series of AJAX requests to the server, and the component itself constantly updates, updates, updates.

Fixing it is very simple. You need to add an empty array of those dependencies on which it depends, and changes in which will restart the effect. If we have an empty list of dependencies specified here, then the effect, accordingly, will not be restarted. This is not some kind of hack, it is a basic feature of using useEffect.

Let's say we fixed it. Now a little complicated. We have a component that renders something that needs to be taken from the server for some kind of ID. In this case, in principle, everything works fine until we change the entityId in the parent, perhaps this is not relevant for your component.

But most likely, if it changes or there is a need to change it, and you have an old component on your page and it turns out that it is not updating, it is better to add entityId here, as a dependency, causing the update, updating the data.

A more complex example with useCallback. Here, at first glance, everything is fine. We have a certain page that has some kind of countdown timer, or, conversely, a timer that just ticks. And, for example, the list of hosts, and the top filters that allow you to filter this list of hosts. Well, maintenance is added here just to illustrate a frequently changing value that translates to a renderer.

, , 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 . , , , .

, , , - , . , , , . . , - - .

— , , - . , , . , , - . , . . Useful links:


All Articles