Is it correct to use useCallback within a custom hook?

Asked

Viewed 253 times

1

I’m having trouble using custom Hooks inside the useEffect, in particular, when defining within the useEffect custom hook methods that alter your internal state. Faced with this problem I came across the following situations.

Model used for the example

import React, { useState } from "react";

const useCounter = () => {
  const [count, setCount] = useState<number>(0);
  const methods = {
    increment: () => setCount((prev) => prev + 1),
  };
  return { count, ...methods };
};

const Counter: React.FC = (props) => {
  const { count, increment } = useCounter();
  return (
    <div>
      <h3>{count}</h3>
      <button onClick={() => increment()}>Increment</button>
    </div>
  );
};
export default Counter;

Supposing it is necessary to execute "increment" in the assembly of the component. This can be done using the useEffect passing an empty dependency array.

  useEffect(() => {
    increment();
    console.log("INCREMENT ON MOUNT...");
  }, []);

  

However, by executing the above code is obtained the warning of the Eslint that "increment" should be added to the dependency array or removed from the useEffect.

Line 17:6: React Hook useEffect has a Missing dependency: 'increment'. Either include it or remove the dependency array React-Hooks/exhaustive-deps

Then the appropriate changes are made:

  useEffect(() => {
    increment();
    console.log("LOOPING...");
  }, [increment]);

Now with "increment" added to the dependencies array of useEffect the warning of Eslint disappears, however, the application goes into looping.

Attempts to resolve

At first I thought that this problem could be related there is lack of referential equality between the functions, since they are recreated with each render. So as soon as I got the problem I tried to solve it using the useCallback, since it provides an equal referential function between each component render;

  const safeIncrement = useCallback(() => increment(), []);

  useEffect(() => {
    safeIncrement();
    console.log("NO LOOPING...");
  }, [safeIncrement]);

The change has returned me a second warning, this time requesting that "increment" was passed as a dependency on useCallback:

Line 14:56: React Hook useCallback has a Missing dependency: 'increment'. Either include it or remove the dependency array React-Hooks/exhaustive-deps

Change made, and flies there, application in looping again:

  const safeIncrement = useCallback(() => increment(), [increment]);

  useEffect(() => {
    safeIncrement();
    console.log("LOOPING...");
  }, [safeIncrement]);

Passing useCallback to custom hook

const useCounter = () => {
  const [count, setCount] = useState<number>(0);
  const methods = {
    increment: useCallback(() => setCount((prev) => prev + 1), []),
  };
  return { count, ...methods };
};

Then I passed the useCallback into the custom hook and it seemed to work, no warning, no looping this time... The point is, I don’t know if the practice is right, I’ve never done it, I’ve never seen anyone do it, and my general approach may be wrong. If anyone can give me feedback about this practice I thank you :)

To check the Renders sequence(seems to be occurring correctly):

import React, { useState, useEffect, useCallback } from "react";

const useCounter = () => {
  const [count, setCount] = useState<number>(0);
  const methods = {
    increment: useCallback(() => {
      setCount((prev) => prev + 1);
      console.log("RENDER - INCREMENT");
    }, []),
  };
  return { count, ...methods };
};

const Counter: React.FC = (props) => {
  const { count, increment } = useCounter();

  useEffect(() => {
    console.log("RENDER - APP");
  });

  useEffect(() => {
    increment();
  }, [increment]);

  return (
    <div>
      <h3>{count}</h3>
      <button onClick={() => increment()}>Increment</button>
    </div>
  );
};
export default Counter;

1 answer

1


I think your problem is in:

const useCounter = () => {

  const methods = {    // DESSA MANEIRA
     ...               // ESSA VARIÁVEL VAI SER SEMPRE RECRIADA E 
  };                   // SUA REFERÊNCIA VAI MUDAR A CADA render DE useCounter
};

Don’t put the useCallback inside the Object methods. I don’t know if it’s gonna work, but that’s against one of the rules of the Hooks and it’s not good practice.

inserir a descrição da imagem aqui

This is one of the ways to do:

const useCounter = () => {
  const [count,setCount] = React.useState(0);
  
  const increment = React.useCallback(() => setCount((prevState) => prevState + 1),[]);
  
  return ({
    count, increment
  });
  
};

function App() {

  const { count, increment } = useCounter();

  return(
    <React.Fragment>
      <div>Count: {count} </div>
      <button onClick={increment}>Increment</button>
    </React.Fragment>
  );
}

ReactDOM.render(<App/>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>

If you really want to use an object called methods, you can do so:

  • useCallback ensures that the reference to increment doesn’t change.
  • useRef ensures that the reference to methods doesn’t change.

const useCounter = () => {
  const [count,setCount] = React.useState(0);
  
  const increment = React.useCallback(() => setCount((prevState) => prevState + 1),[]);
  
  const methods = React.useRef({
    increment
  });
 
  return ({
    count, 
    ...methods.current
  });
  
};

function App() {

  const { count, increment } = useCounter();

  return(
    <React.Fragment>
      <div>Count: {count} </div>
      <button onClick={increment}>Increment</button>
    </React.Fragment>
  );
}

ReactDOM.render(<App/>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>

Browser other questions tagged

You are not signed in. Login or sign up in order to post.