Translation: The rolling curtain is still

Original address: dmitripavlutin.com/react-useef…

react hook useEffect

The React useEffect() hook can be used to handle side effects, such as fetching network requests, manipulating the DOM directly, and starting and ending timers.

Although useEffect() is the most commonly used hook along with useState()(state management hook), useEffect() takes some getting used to and learning how to use it properly.

One pitfall that can be encountered when using useEffect() is the problem of infinite loops in component rendering. This article takes a look at scenarios that create infinite loops and how to avoid them.

If you are not familiar with useEffect(), it is recommended that you first read useEffect Introduction. A good knowledge base can avoid mistakes made by beginners.

1. Infinite loops with side effects update status

Define a functional component that contains an input box. The job now is to count and display the number of times the content of the input box changes.

The
component is implemented as follows:

import { useEffect, useState } from 'react';
function CountInputChanges() {
  const [value, setValue] = useState(' ');
  const [count, setCount] = useState(-1);
  useEffect(() = > setCount(count + 1));
  const onChange = ({ target }) = > setValue(target.value);
  return (
    <div>
      <input type="text" value={value} onChange={onChange} />
      <div>Number of changes: {count}</div>
    </div>)}Copy the code

is a controllable component. The state variable value stores the value of the input box, and the event handle onChange updates the value when the input box changes.

I decided to use the useEffect() hook to change the count variable. UseEffect (() => setCount(count + 1)) updates the counter value every time the component is re-rendered due to input from the user in the input field.

Since useEffect(() => setCount(count + 1)) does not use a dependent argument, the () => setCount(count + 1) callback is executed after each rendering of the component.

Did you anticipate problems with this component? Let’s try running this demo.

This example shows an uncontrolled increment of the count state variable, even if nothing is entered in the input field. This is an example of an infinite loop.

The root of the problem is the way useEffect() is used:

useEffect(() = > setCount(count + 1));
Copy the code

This leads to an infinite loop of component re-rendering.

After the initial rendering, useEffect() performs a side effect callback to update the state, which triggers a re-rendering. After the re-rendering is complete, useEffect() performs a side effect callback and updates the state variables again, which triggers a re-rendering… And so on.

1.1 Fixing dependency parameters

The solution to the infinite loop problem is to pass the correct dependency parameter to useEffect(callback, dependencies).

The purpose of the code is to count the number of times value changes, so you can simply pass in value as a side effect dependency:

import { useEffect, useState } from 'react';
function CountInputChanges() {
  const [value, setValue] = useState(' ');
  const [count, setCount] = useState(-1);
  useEffect(() = > setCount(count + 1), [value]);
  const onChange = ({ target }) = > setValue(target.value);
  return (
    <div>
      <input type="text" value={value} onChange={onChange} />
      <div>Number of changes: {count}</div>
    </div>
  );
}
Copy the code

Use [value] as a useEffect(… , [value]), the count state variable is updated only when [value] changes. That solves the infinite loop problem.

Run the repaired demo online

Now, once you enter something in the input field, the state variable count correctly shows how many times the input field changes.

1.2 Using References

Another alternative to the infinite loop is to store the number of times the contents of the input box change by using a reference (created through the useRef() hook).

The core idea is that updates to references do not trigger re-rendering of components.

Here are the possible implementations:

import { useState, useRef } from 'react';
function CountInputChanges() {
  const [value, setValue] = useState(' ');
  const countRef = useRef(0);
  const onChange = ({ target }) = > {
    setValue(target.value);
    countRef.current++;
  };
  return (
    <div>
      <input type="text" value={value} onChange={onChange} />
      <div>Number of changes: {countRef.current}</div>
    </div>
  );
}
Copy the code

As shown in the code, countref.current ++ inside the event handle onChange is executed every time a value changes. Reference changes do not trigger rerendering.

Running Demo online

Now, as soon as the contents of the input box are changed, the countRef reference is updated and does not trigger a re-rendering – effectively solving the infinite loop problem.

2. Infinite loops and new object references

Even if you set up useEffect() dependencies correctly, you still have to be careful when using object types as dependencies.

For example, the following component CountSecrets listens for user input, and as soon as the user enters the keyword ‘secret’, the secrets counter is increased and displayed.

The following are possible implementations of the components:

import { useEffect, useState } from "react";
function CountSecrets() {
  const [secret, setSecret] = useState({ value: "".countSecrets: 0 });
  useEffect(() = > {
    if (secret.value === 'secret') {
      setSecret(s= >({... s,countSecrets: s.countSecrets + 1}));
    }
  }, [secret]);
  const onChange = ({ target }) = > {
    setSecret(s= > ({ ...s, value: target.value }));
  };
  return (
    <div>
      <input type="text" value={secret.value} onChange={onChange} />
      <div>Number of secrets: {secret.countSecrets}</div>
    </div>
  );
}
Copy the code

Run demo online, enter content in the input box, try typing ‘secret’. As soon as you type ‘secret’, the state variable secret.countsecrets starts to grow uncontrollably.

This also leads to the problem of infinite loops.

Why is that?

Secret object as useEffect(… , [secret]), inside the side effect callback, the status update function is called once the input field is ‘secret’ :

setState(s= >({... s,countSecrets: s.countSecrets + 1}));
Copy the code

The status update function adds the counter countSecrets, but also creates a new object.

Secret is a new object, so the dependency changes. So useEffect (… , [secret]) is called again, the side effect updates the state variable, which in turn causes a new Secret object to be created, and so on.

In JavaScript, two objects are the same only if they reference the same object at the same address.

2.1 Avoid objects as dependencies

The best solution to the problem of infinite loops caused by creating new objects is to avoid using objects as dependencies in useEffect() :

let count = 0;
useEffect(() = > {
  // some logic
}, [count]); // Good!
let myObject = {
  prop: 'Value'
};
useEffect(() = > {
  // some logic
}, [myObject]); // Not good!
useEffect(() = > {
  // some logic
}, [myObject.prop]); // Good!
Copy the code

Fix infinite loop problem in

component need to change dependency, useEffect(… , [secret]) change to useEffect(… Value], [secret.).

It is sufficient to simply call the side effect callback when secret.value changes. Here is the code after the component is fixed:

import { useEffect, useState } from "react";
function CountSecrets() {
  const [secret, setSecret] = useState({ value: "".countSecrets: 0 });
  useEffect(() = > {
    if (secret.value === 'secret') {
      setSecret(s= >({... s,countSecrets: s.countSecrets + 1}));
    }
  }, [secret.value]);
  const onChange = ({ target }) = > {
    setSecret(s= > ({ ...s, value: target.value }));
  };
  return (
    <div>
      <input type="text" value={secret.value} onChange={onChange} />
      <div>Number of secrets: {secret.countSecrets}</div>
    </div>
  );
}
Copy the code

Run the repaired version. Enter something in the input box… As soon as ‘secret’ is entered, the counter will be increased and no infinite loops will be caused.

3. Summary

UseEffect (callback, DEPS) is a hook that performs callbacks (side effects) after a component has rendered, and if you don’t pay attention to what the side effects are doing, you can trigger an infinite loop of component rendering.

A common example of an infinite loop is updating status in a side effect without adding any dependency parameters at all:

useEffect(() = > {
    // Infinite loop!
    setState(count + 1);
});
Copy the code

An effective way to avoid infinite loops is to set dependency parameters properly – to control when side effects should run.

useEffect(() = > {
    // No infinite loop
    setState(count + 1);
}, [whenToUpdateValue]);
Copy the code

Another alternative is to use references. Updating references does not trigger rerendering:

countRef.current++;
Copy the code

Another common way to do an infinite loop is to use an object as a dependency on useEffect() and update the object in a side effect (creating a new object) :

useEffect(() = > {
    // Infinite loop!setObject({ ... object,prop: 'newValue'})},object]);
Copy the code

Avoid using objects as dependencies and stick to specific attributes (the end result should be a raw value) :

useEffect(() = > {
    // No infinite loopsetObject({ ... object,prop: 'newValue'})},object.whenToUpdateProp]);
Copy the code

What are other common problems with Using React hooks? In my previous article, I listed five mistakes to avoid when using React hooks

Do you know of any other ways to trigger an infinite loop trap using useEffect()?

Leave a comment and like it in the comments section

Small praise, big motivation ❤️❤️❤️