React Rendering : useRef , useSate and useEffect

React Rendering : useRef , useSate and useEffect

October 8, 2024

renderingreact hooks

Understanding React Rendering and Infinite Loops with useState, useEffect, and useRef

In React, it’s crucial to understand how rendering works, especially when using hooks like useState, useEffect, and useRef. These hooks allow us to manage state, perform side effects, and reference values without re-rendering, respectively. However, when not used carefully, they can lead to unexpected behaviors, such as infinite rendering loops or excessive re-renders.

In this article, we'll dissect a React code snippet that causes infinite renders and dive into why this happens. Additionally, we’ll explore the use of useRef, why it behaves differently from useState, and why certain hooks cause multiple renders even when you expect only one.

The Problematic Code: An Infinite Render Loop

Let’s start with the following React component:

import { useState, useEffect, useRef } from 'react';

export default function App() {
  const [inputValue, setInputValue] = useState('');
  const [render, setRender] = useState(0);
  const count = useRef(0);

  useEffect(() => {
    count.current = count.current + 1;
  });

  useEffect(() => {
    setRender(() => render + 1);
  });

  return (
    <>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <h4>Render Count (useRef): {count.current}</h4>
      <h4>Render Count (useState): {render}</h4>
    </>
  );
}

This code seems simple, but as soon as you type something into the input field, you'll notice the application starts re-rendering infinitely. Why does this happen?

Identifying the Infinite Loop

Key Cause: Misusing useEffect

The primary reason for the infinite loop lies in this useEffect hook:

useEffect(() => {
  setRender(() => render + 1);
});

Here’s why this causes an infinite loop:

  1. No Dependency Array:
    • When useEffect is used without a dependency array, it runs after every render.
    • Each time the component renders, this useEffect runs, which triggers the state update via setRender.
  2. State Update (setRender):
    • Calling setRender(() => render + 1) updates the render state.
    • Any state update triggers a new render in React. Therefore, the component re-renders whenever render changes.
  3. Re-render Loop:
    • After every re-render, useEffect runs again, triggering another setRender, which causes yet another re-render. This creates an infinite render loop.

How React Triggers Re-renders

To understand why this loop happens, it’s helpful to know how React handles state updates:

  • useState updates are asynchronous. When you call setRender(), React doesn’t update the state immediately. Instead, it schedules a re-render after the current function finishes.
  • After React re-renders the component, all useEffect hooks without dependencies will run again.

Thus, the component enters a vicious cycle:

  1. Initial render happens.
  2. useEffect runs, calling setRender().
  3. setRender() updates the state, causing another re-render.
  4. After the re-render, useEffect runs again, triggering setRender() again, and the loop continues indefinitely.

Fixing the Infinite Loop: Adding a Dependency Array

To fix this issue, you need to ensure that the useEffect hook only runs when necessary. By adding a dependency array, we can control when the effect should run. For instance, if we want to update the render count only when inputValue changes, we can write:

useEffect(() => {
  setRender((prevRender) => prevRender + 1);
}, [inputValue]); // Only runs when inputValue changes

Now, the useEffect hook only runs when inputValue changes, and setRender is only called when there's a change in the input field.

The Role of useRef and Why It Doesn’t Cause Re-renders

In the code, useRef is used to track the number of renders. Specifically, count.current is incremented on every render:

useEffect(() => {
  count.current = count.current + 1;
});

Let’s break down how useRef works and why it doesn’t contribute to re-renders:

  1. Persistent Across Renders:
    • The value stored in useRef (i.e., count.current) persists between renders but changing it does not cause a re-render. This is a key difference from useState.
  2. Mutable Object:
    • useRef returns a mutable object with a .current property. You can freely update this value, but since React doesn't monitor changes in useRef, no re-render is triggered.
  3. Tracking Without Re-rendering:
    • Since useRef is updated on every render without causing a new render, it is perfect for tracking how many times the component has rendered.

Why Use useRef Over useState?

In the context of this component, useRef is used to track the total number of renders (count.current), while useState is used to track the number of renders triggered by inputValue changes (render). The key distinction is:

  • useRef doesn't cause re-renders when its value changes.
  • useState does cause re-renders when its value changes.

Why Two Renders Occur on Input Change

You might have noticed that even after fixing the infinite loop, changing inputValue still causes two renders. Let’s understand why.

1. First Render (Caused by inputValue Change)

When you type into the input field, the onChange handler triggers:

onChange={(e) => setInputValue(e.target.value)}

This calls setInputValue(), which updates the inputValue state. Since any state change causes a re-render, React re-renders the component. During this re-render:

  • The first useEffect increments count.current by 1.
  • The second useEffect runs because inputValue changed and calls setRender.

2. Second Render (Caused by setRender)

In the second useEffect:

useEffect(() => {
  setRender((prevRender) => prevRender + 1);
}, [inputValue]);

The call to setRender updates the render state, which causes another render. This is the second render.

React’s Rendering Cycle

  1. The first render is caused by the change in inputValue.
  2. The second render is caused by the setRender call inside useEffect.

Although useRef updates its value on every render, it does not cause these renders because useRef updates don’t trigger re-renders.

Conclusion

In this article, we explored the common pitfalls of working with React hooks, specifically useState, useEffect, and useRef. We saw how improper usage of useEffect can lead to infinite render loops and how useRef can be used to track values without triggering re-renders.

Key Takeaways:

  • Infinite loops can occur when useEffect runs on every render without a dependency array and triggers a state update.
  • useState updates cause re-renders, while useRef updates do not.
  • React’s asynchronous state updates and effect executions after renders can sometimes lead to multiple renders even when you expect only one.

By understanding these principles, you can avoid common issues like infinite render loops and ensure that your React components render efficiently and correctly.