ZiiMakc
ZiiMakc

Reputation: 37026

Skip first useEffect render custom hook

I want to make custom hook that will skip first useEffect render and reuse it everywhere.

I made a working example of custom hook:

function useEffectSkipFirst(fn, arr) {
  const isFirstRun = useRef(true);

  useEffect(() => {
    if (isFirstRun.current) {
      isFirstRun.current = false;
      return;
    }

    fn();
  }, [...arr]);
}

How to use:

useEffectSkipFirst(fn, []);

Problem is that i got a few warnings that i try to understand and hope you can help me:

React Hook useEffect has a missing dependency: 'fn'. Either include it or remove the dependency array. If 'fn' changes too often, find the parent component that defines it and wrap that definition in useCallback. (react-hooks/exhaustive-deps) eslint

React Hook useEffect has a spread element in its dependency array. This means we can't statically verify whether you've passed the correct dependencies. (react-hooks/exhaustive-deps)

Codesandbox example.

Full example code:

import React, { useRef, useEffect, useState } from "react";
import ReactDOM from "react-dom";

function useEffectSkipFirst(fn, arr) {
  const isFirstRun = useRef(true);

  useEffect(() => {
    if (isFirstRun.current) {
      isFirstRun.current = false;
      return;
    }

    fn();
  }, [...arr]);
}

function App() {
  const [clicks, setClicks] = useState(0);
  const [date, setDate] = useState(Date.now());
  const [useEffectRunTimes, setTseEffectRunTimes] = useState(0);

  useEffectSkipFirst(() => {
    addEffectRunTimes();
  }, [clicks, date]);

  function addEffectRunTimes() {
    setTseEffectRunTimes(useEffectRunTimes + 1);
  }

  return (
    <div className="App">
      <div>clicks: {clicks}</div>
      <div>date: {new Date(date).toString()}</div>
      <div>useEffectRunTimes: {useEffectRunTimes}</div>
      <button onClick={() => setClicks(clicks + 1)}>add clicks</button>
      <button onClick={() => setDate(Date.now())}>update date</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Upvotes: 2

Views: 1774

Answers (3)

Olivier Boiss&#233;
Olivier Boiss&#233;

Reputation: 18173

I see one solution to your problem.

First you need to reduce the array of useEffect dependencies to a single value (by using the hook useMemo).

Then in the effect, you should use useRef to store the old value of this dependency value, so you will be able to call the fn function only when the dependency has changed.

Here is the code of the custom hook :

function useEffectSkipFirst(fn, depValue) {
  const oldDepValueRef = useRef(depValue);

  useEffect(() => {
    if (depValue !== oldDepValueRef.current) {
      // Note that this code will not be executed the first time as
      // we initialized oldDepValueRef with depValue
      fn();
      oldDepValueRef.current = depValue;
    }
  }, [fn, depValue]);
}

Here is the code to reduce the array dep to a single value using useMemo :

// I decided the return an object but it could be an array or something else
const depValue = useMemo(() => ({}), [clicks, date]);

useEffectSkipFirst(addEffectRunTimes, depValue);

function addEffectRunTimes() {
  // When the value depends on the previous value, 
  // you can use a function to update the state value
  setTseEffectRunTimes(v => v + 1);
}

By the way if the addEffectRunTimes function is only used in the effect, you could directly write :

useEffectSkipFirst(() => setTseEffectRunTimes(v => v + 1), depValue);

Upvotes: 1

Clarity
Clarity

Reputation: 10873

The first warning basically means that all the dependencies referenced inside useEffect's callback have to be declared inside its dependencies array. The official documentation provides more explanation about adding functions to the dependency array.

The second waring means that you can't specify dependencies dynamically, i.e. in a way which makes them not known until runtime. This is because the dependencies are compared statically before code execution and therefore have to be spelled out explicitly.

Upvotes: 1

Shubham Khatri
Shubham Khatri

Reputation: 282040

The fn function which you take as a callback to useEffectSkipFirst changes on every render. Also since this function is being called within useEffectSkipFirst, the linter prompts you to define it as a dependency.

However, the way you implement it, its upto the code that implements useEffectSkipFirst to take care that all values have the correct closures and you can skip defining it as a dependency to useEffect. If however you do, it might lead to an infinite loop.

You may check this post too for more details: How to fix missing dependency warning when using useEffect React Hook?

Upvotes: 1

Related Questions