skip to Main Content

I often find myself needing to prevent useEffect from running on the initial render and only triggering it when dependencies change. To make this easier, I want to create a custom hook that handles this. How can I create such a hook in React?

This is my code:

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

export default function Counter() {
  const [count, setCount] = useState(0);


  useEffect(() => {
    // i want to save the count variable only when count is updated
    localStorage.setItem('count', count + '') // but it set the localStorage count to 0, making my code useless
  }, [count]);


  useEffect(() => {
    // load save count if available
    let count = localStorage.getItem('count')
    if(count) {
       setCount(parseInt(count))
    }
  }, [])

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

This code saves count into the localstorage every time the component loads, so count is set to 0 every time, and when its time to load the count variable from the localStorage, it reads 0

How can I create a React hook to run effectful code only when one of the deps change?

3

Answers


  1. This sounds like an X/Y problem. If you want to only save the count variable when it’s updated, wrap setCount accordingly.

    Something like this…

    function useStateSavedToStorage(key, initial) {
      const [value, _setValue] = React.useState(() => {
        const valueFromStorage = localStorage.getItem(key);
        // Doesn't support storing the value `null`, but it's otherwise
        // tedious to know whether a value exists in local storage but is
        // null, or doesn't exist at all.
        if(valueFromStorage !== null) return valueFromStorage;
        // TODO: support the function form of useState's `initial`
        return initial;
      });
      const setValue = React.useCallback((newValue) => {
        // TODO: support the function form for `setValue((x) => ...)`
        localStorage.setItem(key, newValue);
        _setValue(newValue);
      }, []);
      return [value, setValue];
    }
    
    Login or Signup to reply.
  2. You can simply return false if count is equal to 0 (first useEffect call on mount)

        useEffect(() => {
            if(!count){
            return
            }
    //....
          }, [count])
    
    Login or Signup to reply.
  3. You want a ref to track whether the component has mounted.

    The Hook

    import { useEffect, useRef } from 'react';
    
    export function useUpdateEffect(callback, dependencies) {
        // Ref to track the initial mount
        const isFirstRender = useRef(true);
    
        useEffect(() => {
            // Skip the effect on the first render
            if (isFirstRender.current) {
                isFirstRender.current = false;
                return;
            }
    
            // Run the callback on subsequent renders when dependencies change
            return callback();
        }, dependencies);
    }
    

    Example Usage

    The useUpdateEffect hook can be used similarly to useEffect, but it will skip execution on the component’s initial render due to the if statement at the top

    import React, { useState } from 'react';
    import useUpdateEffect from './useUpdateEffect';
    
    function ExampleComponent() {
        const [count, setCount] = useState(0);
    
        useUpdateEffect(() => {
            // This code will not run on the first render
            console.log('Count has changed:', count);
        }, [count]);
    
        return (
            <div>
                <h1>{count}</h1>
                <button onClick={() => setCount(count + 1)}>Increment</button>
            </div>
        );
    }
    

    If you’re wondering about why I decided to return the callback, this is for whenever you need to run a clean up when the component unmounts to prevent a memory leak.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search