Working with callbacks in React

  • Tutorial

During my work, I occasionally came across the fact that developers do not always clearly understand how the data transfer mechanism works through props, in particular callbacks, and why their PureComponents are updated so often.


Therefore, in this article we will understand how callbacks are transmitted to React, and also discuss the peculiarities of the work of event handlers.


TL; DR


  1. Do not interfere with JSX and business logic - this will complicate code perception.
  2. For small optimizations, cache function handlers in the form of classProperties for classes or with useCallback for functions — then pure components will not be rendered all the time. Especially caching of callbacks can be useful so that when they are transferred to PureComponent, unnecessary updating cycles do not occur.
  3. Do not forget that in Kolbek you get not a real event, but a Syntetic event. If you exit the current function, you will not be able to access the fields of this event. Cache the fields you need if you have closures with asynchrony.

Part 1. Event handlers, caching and code perception


React is a fairly convenient way to add event handlers for html elements.


This is one of the basic things that any developer gets to know when he starts writing on React:


classMyComponentextendsComponent{
  render() {
    return<buttononClick={() => console.log('Hello world!')}>Click me</button>;
  }
}

Simple enough? From this code it becomes immediately clear what will happen when the user clicks on the button.


But what if the code in the handler is getting bigger and bigger?


Suppose that by the button we have to load and filter all those who are not included in a certain command ( user.team === 'search-team'), then sort them by age.


classMyComponentextendsComponent{
  constructor(props) {
    super(props);
    this.state = { users: [] };
  }
  render() {
    return (
      <div><ul>
          {this.state.users.map(user => (
            <li>{user.name}</li>
          ))}
        </ul><buttononClick={() => {
            console.log('Hello world!');
            window
              .fetch('/usersList')
              .then(result => result.json())
              .then(data => {
                const users = data
                  .filter(user => user.team === 'search-team')
                  .sort((a, b) => {
                    if (a.age > b.age) {
                      return 1;
                    }
                    if (a.age < b.age) {
                      return-1;
                    }
                    return0;
                  });
                this.setState({
                  users:users,
                });
              });
          }}
        >
          Load users
        </button></div>
    );
  }
}

This code is quite difficult to understand. The code of business logic is mixed with the layout that the user sees.


The easiest way to get rid of this: put the function on the level of class methods:


classMyComponentextendsComponent{
  fetchUsers() {
    // Выносим код сюда
  }
  render() {
    return (
      <div><ul>
          {this.state.users.map(user => (
            <li>{user.name}</li>
          ))}
        </ul><buttononClick={() => this.fetchUsers()}>Load users</button></div>
    );
  }
}

Here we have taken the business logic from the JSX code into a separate field in our class. To make this available inside a function, we define a callback like this:onClick={() => this.fetchUsers()}


In addition, when describing a class, we can declare a field as an arrow function:


classMyComponentextendsComponent{
  fetchUsers = () => {
    // Выносим код сюда
  };
  render() {
    return (
      <div><ul>
          {this.state.users.map(user => (
            <li>{user.name}</li>
          ))}
        </ul><buttononClick={this.fetchUsers}>Load users</button></div>
    );
  }
}

This will allow us to declare colback as onClick={this.fetchUsers}


What is the difference between these two ways?


onClick={this.fetchUsers}- Here, each time the render function is called in props k button, the same link will always be transmitted.


In the case of with onClick={() => this.fetchUsers()}each call to the function, the render JavaScript initializes the new function () => this.fetchUsers()and sets it to onClickprop. This means that nextProp.onClick, and prop.onClickin buttonthis case will always be not equal, even if the component will be marked as clean, it will pererenderen.


What does it threaten during development?


In most cases, visually, you will not notice a performance drop, because the Virtual DOM that will be generated by the component will not be different from the previous one, and there will be no changes in your DOM.


However, if you render large lists of components or tables, you will notice "brakes" on a large amount of data.


Why is understanding how a function is transferred to a calbek important?


Often you can come across such tips on twitter or on stackoverflow:


"If you have performance problems with React applications, try replacing inheritance from Component with PureComponent. Also, do not forget that for Component you can always determine shouldComponentUpdate to get rid of unnecessary updating cycles."


If we define a component as Pure, it means that it already has a function shouldComponentUpdatethat makes shallowEqual between props and nextProps.


Each time we pass a new kolbek function to such a component, we lose all the advantages and optimization PureComponent.


Let's look at an example.
Create the Input component, which will also display information how many times it has been updated:


classInputextendsPureComponent{
  renderedCount = 0;
  render() {
    this.renderedCount++;
    return (
      <div><inputonChange={this.props.onChange} /><p>Input component was rerendered {this.renderedCount} times</p></div>
    );
  }
}

Create two components that will render the Input inside:


classAextendsComponent{
  state = { value: '' };
  onChange = e => {
    this.setState({ value: e.target.value });
  };
  render() {
    return (
      <div><InputonChange={this.onChange} /><p>The value is: {this.state.value} </p></div>
    );
  }
}

And the second:


classBextendsComponent{
  state = { value: '' };
  onChange(e) {
    this.setState({ value: e.target.value });
  }
  render() {
    return (
      <div><InputonChange={e => this.onChange(e)} />
        <p>The value is: {this.state.value} </p></div>
    );
  }
}

You can try the example by hand here: https://codesandbox.io/s/2vwz6kjjkr
This example clearly demonstrates how you can lose all the advantages of PureComponent if you pass a new callback function to PureComponent each time.


Part 2. Using Event handlers in function components


In the new version of React (16.8), the mechanism React hooks was announced , which allows you to write full functional components with a clear lifecycle that can cover almost all of the casecases that until now covered only classes.


We modify the example with the Input component so that all components are represented by a function and work with React-hooks.


Input must keep inside itself information about how many times it has been changed. If in the case of classes we used a field in our instance, access to which was implemented through this, then in the case of a function we cannot declare a variable through this.
React provides a useRef hook, with which you can save a reference to the HtmlElement in the DOM tree, but it is also interesting because it can be used for the regular data our component needs:


import React, { useRef } from'react';
exportdefaultfunctionInput({ onChange }) {
  const componentRerenderedTimes = useRef(0);
  componentRerenderedTimes.current++;
  return (
    <>
      <input onChange={onChange} />
      <p>Input component was rerendered {componentRerenderedTimes.current} times</p>
    </>
  );
}

We also need the component to be "clean", that is, updated only if the props that were transferred to the component have changed.
To do this, there are different libraries that provide HOC, but it is better to use the memo function, which is already built into React, because it works faster and more efficiently:


import React, { useRef, memo } from'react';
exportdefault memo(functionInput({ onChange }) {
  const componentRerenderedTimes = useRef(0);
  componentRerenderedTimes.current++;
  return (
    <>
      <input onChange={onChange} />
      <p>Input component was rerendered {componentRerenderedTimes.current} times</p>
    </>
  );
});

Input component is ready, now we rewrite components A and B.
In the case of component B, this is easy to do:


import React, { useState } from'react';
functionB() {
  const [value, setValue] = useState('');
  return (
    <div><InputonChange={e => setValue(e.target.value)} />
      <p>The value is: {value} </p></div>
  );
}

Here we used a useStatehook that allows you to save and work with the state of the component, in case the component is represented by a function.


How can we cache function callback? We cannot remove it from the component, since in this case it will be common for different component instances.
For such tasks, React has a set of caching and memory hooks, of which https://reactjs.org/docs/hooks-reference.html is the most suitable for us.useCallback


Add Athis hook to the component :


import React, { useState, useCallback } from'react';
functionA() {
  const [value, setValue] = useState('');
  const onChange = useCallback(e => setValue(e.target.value), []);
  return (
    <div><InputonChange={onChange} /><p>The value is: {value} </p></div>
  );
}

We cache the function, which means the Input component will not be updated every time.


How does useCallbackhook work ?


This hook returns the cached function (that is, the link does not change from the renderer to the renderer).
In addition to the function that needs to be cached, the second argument is passed to it - an empty array.
This array allows you to transfer a list of fields when changing which you want to change the function, i.e. return new link.


Clearly the difference between the usual way of transferring the function to the callback and useCallbackcan be found here: https://codesandbox.io/s/0y7wm3pp1w


Why do we need an array?


Suppose we need to cache a function that depends on some value through a closure:


import React, { useCallback } from'react';
import ReactDOM from'react-dom';
import'./styles.css';
functionApp({ a, text }) {
  const onClick = useCallback(e => alert(a), [
    /*a*/
  ]);
  return<buttononClick={onClick}>{text}</button>;
}
const rootElement = document.getElementById('root');
ReactDOM.render(<Apptext={'Clickme'} a={1} />, rootElement);

Here the App component depends on prop a. If we run the example, then everything will work correctly until we add to the end:


setTimeout(() => ReactDOM.render(<Apptext={'NextA'} a={2} />, rootElement), 5000);

After the timeout is triggered, a click on the button in the alert will be displayed 1. This happens because we have saved the previous function, which has closed the avariable. And since it ais a variable, which in our case is the value type, and the value type is immutable, we received this error. If we remove the comment /*a*/, the code will work correctly. React in the second render will verify that the data passed in the array is different and will return a new function.


You can try this example yourself here: https://codesandbox.io/s/6vo8jny1ln


React provides many functions that allow you to memorize data, for example useRef, useCallbackand useMemo.
If the latter is needed to memorize the values ​​of a function, and they are useCallbackquite similar to each other, then useRefit allows you to cache not only references to DOM elements, but also to act as an instance field.


At first glance, it can be used to cache functions, because it useRefalso caches data between individual component updates.
However, it is useRefundesirable to use for caching functions. If our function uses a closure, then in any render the closed value may change, and our cached function will work with the old value. This means that we will need to write the update logic of the functions or simply use useCallbackit in which it is implemented through the dependency mechanism.


https://codesandbox.io/s/p70pprpvvx here you can see the memoratization of functions with the correct useCallback, with the wrong and with useRef.


Part 3. Syntetic events


We have already figured out how to use event handlers and how to work correctly with closures in callbacks, but React has another very important difference when working with them:


Let's pay attention: now Input, with which we worked above, is absolutely synchronous, but in some cases it may be necessary for the colback to occur with a delay, according to the debounce or throttling pattern . So, debounce, for example, is very convenient to use for the search line input — the search will occur only when the user stops typing characters.


Create a component that internally causes a state change:


functionSearchInput() {
  const [value, setValue] = useState('');
  const timerHandler = useRef();
  return (
    <>
      <input
        defaultValue={value}
        onChange={e => {
          clearTimeout(timerHandler.current);
          timerHandler.current = setTimeout(() => {
            setValue(e.target.value);
          }, 300); // wait, if user is still writing his query
        }}
      />
      <p>Search value is {value}</p>
    </>
  );
}

This code will not work. The fact is that React internally proxies events, and the so-called Syntetic Event gets into our onChange, which after our function will be “cleared” (the fields will be marked in null). React does it for performance reasons, to use one object, rather than creating a new one each time.


If we need to take value, as in this example, then it is enough to cache the required fields BEFORE exiting the function:


functionSearchInput() {
  const [value, setValue] = useState('');
  const timerHandler = useRef();
  return (
    <>
      <input
        defaultValue={value}
        onChange={e => {
          clearTimeout(timerHandler.current);
          const pendingValue = e.target.value; // cached!
          timerHandler.current = setTimeout(() => {
            setValue(pendingValue);
          }, 300); // wait, if user is still writing his query
        }}
      />
      <p>Search value is {value}</p>
    </>
  );
}

See an example here: https://codesandbox.io/s/oj6p8opq0z


In very rare cases, it becomes necessary to save the entire instance of an event. To do this, you can call it event.persist(), which will remove
this Syntetic event instance from the event-pool of the reactor events.


Conclusion:


React event handlers are very convenient, as they are:


  1. Automate subscription and unsubscribe (with unmount component);
  2. Simplify the perception of the code, most of the subscriptions are easy to track in JSX code.

But at the same time, when developing applications, you may encounter some difficulties:


  1. Redefine callbacks to props;
  2. Syntetic events, which are cleared after the current function.

Redefinition of callbacks is usually not noticeable, since vDOM does not change, but it is worth remembering if you enter optimizations, replacing components with Pure via inheritance from PureComponentor using memo, then you should attend to caching them, otherwise the benefits of introducing PureComponents or memo will not be noticeable. For caching, you can use either classProperties (when working with a class) or useCallbackhook (when working with functions).


Для правильной асинхронной работы, в случае если вам нужны данные из события, также кешируйте нужные вам поля.


Also popular now: