DEV Community

Nik
Nik

Posted on

Dealing with callbacks as props in React

TL;DR

  1. Don't mix JSX and business-logic in one place, keep your code simple and understandable.
  2. For small optimizations, you can cache function in class properties for classes or use the useCallback hook for function components. In this case, pure components won't be re-rendered every time when their parent gets re-rendered. Especially, callbacks caching is effective to avoid excess updating cycles when you pass functions as a prop to PureComponents.
  3. Don't forget that event handler receives a synthetic event, not the original event. If you exit from the current function scope, you won't get access to synthetic event fields. If you want to get fields outside the function scope you can cache fields you need.

Part 1. Event handlers, caching and code readability

React has quite a convenient way to add event handlers for DOM elements.
This is one of the first basic things that beginners face with.

class MyComponent extends Component {
  render() {
    return <button onClick={() => console.log('Hello world!')}>Click me</button>;
  }
}

It's quite easy, isn't it? When you see this code, it isn't complicated to understand what will happen when a user clicks the button.
But what should we do, if the amount of the code in event handlers become more and more?
Let's assume, we want to load the list of developers, filter them (user.team === 'search-team') and sort using their age when the button was clicked:

class MyComponent extends Component {
  constructor(props) {
    super(props);
    this.state = { users: [] };
  }
  render() {
    return (
      <div>
        <ul>
          {this.state.users.map(user => (
            <li>{user.name}</li>
          ))}
        </ul>
        <button
          onClick={() => {
            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;
                    }
                    return 0;
                  });
                this.setState({
                  users: users,
                });
              });
          }}
        >
          Load users
        </button>
      </div>
    );
  }
}

This code is so complicated. The business-logic part is mixed with JSX elements.
The simplest way to avoid it is to move function to class properties:

class MyComponent extends Component {
  fetchUsers() {
    // Move business-logic code here
  }
  render() {
    return (
      <div>
        <ul>
          {this.state.users.map(user => (
            <li>{user.name}</li>
          ))}
        </ul>
        <button onClick={() => this.fetchUsers()}>Load users</button>
      </div>
    );
  }
}

We moved business-logic from JSX code to separated field in our class. The business-logic code needs to get access to this, so we made the callback as: onClick={() => this.fetchUsers()}

Besides it, we can declare fetchUsers class field as an arrow function:

class MyComponent extends Component {
  fetchUsers = () => {
    // Move business-logic code here
  };
  render() {
    return (
      <div>
        <ul>
          {this.state.users.map(user => (
            <li>{user.name}</li>
          ))}
        </ul>
        <button onClick={this.fetchUsers}>Load users</button>
      </div>
    );
  }
}

It allows us to declare callback as onClick={this.fetchUsers}

What is the difference between them?

When we declare callback as onClick={this.fetchUsers} every render call will pass the same onClick reference to the button.
At the time, when we use onClick={() => this.fetchUsers()} each render call will init new function () => this.fetchUsers() and will pass it to the button onClick prop. It means, that nextProp.onClick and prop.onClick won't be equal and even if we use a PureComponent instead of button it will be re-rendered.

Which negative effects can we receive during the development?

In the vast majority of cases, we won't catch any visual performance issues, as Virtual DOM doesn't get any changes and nothing is re-rendered physically.
However, if we render big lists of components, we can catch lags on a big amount of data.

Why it's important to understand how functions are passed to the prop?

You can often find on Twitter or StackOverflow such advice:

"If you have issues with performance in React application, try to change inheritance in problem places from Component to PureComponent, or define shouldComponentUpdate to get rid of excess updating cycles".

If we define a component as a PureComponent, it means, that it has already the shouldComponentUpdate function, which implements shallowEqual between its props and nextProps.

If we set up new references as props to PureComponent in updating lifecycle, we'll lose all PureComponent advantages and optimizations.

Let's watch an example.
We implement Input component, that will show a counter representing the number of its updates

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

Now we create two components, which will render the Input component:

class A extends Component {
  state = { value: '' };
  onChange = e => {
    this.setState({ value: e.target.value });
  };
  render() {
    return (
      <div>
        <Input onChange={this.onChange} />
        <p>The value is: {this.state.value} </p>
      </div>
    );
  }
}

Second:

class B extends Component {
  state = { value: '' };
  onChange(e) {
    this.setState({ value: e.target.value });
  }
  render() {
    return (
      <div>
        <Input onChange={e => this.onChange(e)} />
        <p>The value is: {this.state.value} </p>
      </div>
    );
  }
}

You can try the example here: https://codesandbox.io/s/2vwz6kjjkr
This example shows how we can lose all the advantages of PureComponents if we set the new references to the PureComponent every time in the render.

Part 2. Using event handlers in function components

The new React hooks mechanism was announced in the new version of React@16.8 (https://reactjs.org/docs/hooks-intro.html). It allows implementing full-featured function components, with full lifecycle built with hooks. You are able to change almost all class components to functions using this feature. (but it isn't necessary)

Let's rewrite Input Component from classes to functions.

Input should store the information about how many times it was re-rendered. With classes, we are able to use instance field via this keyword. But with functions, we can't declare a variable with this. React provides useRef hook which we can use to store the reference to the HtmlElement in DOM tree. Moreover useRef is handy to store any mutable data like instance fields in classes:

import React, { useRef } from 'react';

export default function Input({ onChange }) {
  const componentRerenderedTimes = useRef(0);
  componentRerenderedTimes.current++;

  return (
    <>
      <input onChange={onChange} />
      <p>Input component was rerendered {componentRerenderedTimes.current} times</p>
    </>
  );
}

We created the component, but it isn't still PureComponent. We can add a library, that gives us a HOC to wrap component with PureComponent, but it's better to use the memo function, which has been already presented in React. It works faster and more effective:

import React, { useRef, memo } from 'react';

export default memo(function Input({ onChange }) {
  const componentRerenderedTimes = useRef(0);
  componentRerenderedTimes.current++;

  return (
    <>
      <input onChange={onChange} />
      <p>Input component was rerendered {componentRerenderedTimes.current} times</p>
    </>
  );
});

Our Input component is ready. Now we'll rewrite A and B components.
We can rewrite the B component easily:

import React, { useState } from 'react';
function B() {
  const [value, setValue] = useState('');

  return (
    <div>
      <Input onChange={e => setValue(e.target.value)} />
      <p>The value is: {value} </p>
    </div>
  );
}

We have used useState hook, which works with the component state. It receives the initial value of the state and returns the array with 2 items: the current state and the function to set the new state. You can call several useState hooks in the component, each of them will be responsible for its own part of the instance state.

How can we cache a callback? We aren't able to move it from component code, as it would be common for all different component instances.
For such kind of issues React has special hooks for caching and memoization. The handiest hook for us is useCallback https://reactjs.org/docs/hooks-reference.html

So, A component is:

import React, { useState, useCallback } from 'react';
function A() {
  const [value, setValue] = useState('');

  const onChange = useCallback(e => setValue(e.target.value), []);

  return (
    <div>
      <Input onChange={onChange} />
      <p>The value is: {value} </p>
    </div>
  );
}

We cached function so that Input component won't be re-rendered every time when its parent re-renders.

How does useCallback work?

This hook returns the memoized version of the function. (that meant the reference won't be changed on every render call).
Beside the function which will be memoized, this hook receives a second argument. In our case, it was an empty array.
The second argument allows passing to the hook the list of dependencies. If at least one of this fields gets changed, the hook will return a new version of the function with the new reference to enforce the correct work of your component.

The difference between inline callback and memoized callback you can see here: https://codesandbox.io/s/0y7wm3pp1w

Why array of dependencies is needed?

Let's suppose, we have to cache a function, which depends on some value via the closure:

import React, { useCallback } from 'react';
import ReactDOM from 'react-dom';

import './styles.css';

function App({ a, text }) {
  const onClick = useCallback(e => alert(a), [
    /*a*/
  ]);

  return <button onClick={onClick}>{text}</button>;
}
const rootElement = document.getElementById('root');
ReactDOM.render(<App text={'Click me'} a={1} />, rootElement);

The component App depends on a prop. If we execute the example, everything will work correctly. However as we add to the end re-render, the behavior of our component will be incorrect:

setTimeout(() => ReactDOM.render(<App text={'Next A'} a={2} />, rootElement), 5000);

When timeout executes, click the button will show 1 instead of 2. It works so because we cached the function from the previous render, which made closure with previous a variable. The important thing here is when the parent gets re-rendered React will make a new props object instead of mutating existing one.
If we uncomment /*a*/ our code will work correctly. When component re-renders the second time, React hook will check if data from deps have been changed and will return the new function (with a new reference).

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

React has a number of functions, which allow memoizing data: useRef, useCallback and useMemo.
The last one is similar to useCallback, but it is handy to memoize data instead of functions. useRef is good both to cache references to DOM elements and to work as an instance field.

At first glance, useRef hook can be used to cache functions. It's similar to instance field which stores methods. However, it isn't convenient to use for function memoization. If our memoized function uses closures and the value is changed between renders the function will work with the first one (that was cached). It means we have to change references to the memoized function manually or just use useCallback hook.

https://codesandbox.io/s/p70pprpvvx — here is the example with the right useCallback usage and wrong useRef one.

Part 3. Synthetic events

We've already watched how to use event handlers, how to work with closures in callbacks but React also have differences in event objects inside event handlers.

Take a look at the Input component. It works synchronously. However, in some cases, you would like to implement debounce or throttling patterns. Debounce pattern is quite convenient for search fields, you enforce search when the user has stopped inputting symbols.

Let's create a component, which will call setState:

function SearchInput() {
  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 won't work. React proxies events and after synchronous callback React cleanups the event object to reuse it in order to optimization. So our onChange callback receives Synthetic Event, that will be cleaned.

If we want to use e.target.value later, we have to cache it before the asynchronous code section:

function SearchInput() {
  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>
    </>
  );
}

Example: https://codesandbox.io/s/oj6p8opq0z

If you have to cache the whole event instance, you can call event.persist(). This function removes your Synthetic event instance from React event-pool. But in my own work, I've never faced with such a necessity.

Conclusion:

React event handlers are pretty convenient as they

  1. implement subscription and unsubscription automatically
  2. simplify our code readability

Although there are some points which you should remember:

  1. Callbacks redefinition in props
  2. Synthetic events

Callbacks redefinition usually doesn't make the big influence on visual performance, as DOM isn't changed. But if you faced with performance issues, and now you are changing components to Pure or memo pay attention to callbacks memoization or you'll lose any profit from PureComponents. You can use instance fields for class components or useCallback hook for function components.

Top comments (3)

Collapse
 
gyurcigyurma profile image
Gy

Thank you, this is absolutely valuable!

Collapse
 
jmonteros81 profile image
jmonteros81

hi Nikita, i can have props from component1 on component2 and receive in the seem component2 the callback of other component3
*******************on component2*****************
const CourseList = (props,{increment}) => {


pls check my code:
github.com/jmonteros81/Building_-A...

Collapse
 
monfernape profile image
Usman Khalil

Pure gold