Infinite loop React hooks

I don't understand why I'm getting infinite loop in useClick I see that I change state value inside useEffect using setVal but useEffect should work only on onClick as specified in second param. I thought that it is because the param onClick i pass is memoized but the callback is not called(i checked that using console.log('go set')

function useClick(onClick, setVal, val) {
  React.useEffect(() => {
    console.log('Click');
    setVal(val + 1);
  }, [onClick]);
}

const Home = () => {
  const [val, setVal] = React.useState(0);
  const incrementOnClick = React.useCallback(() => {
    console.log('go set');
    setVal(val + 1);
  } , [setVal, val]);
  useClick(incrementOnClick, setVal, val);
  return <div>
    <div>{val}</div>
    <button onClick={incrementOnClick}>Click me</button>
 </div>
}

Upvotes: 4

Views: 11478

Answers (2)

Keith
Keith

Reputation: 24181

Looking at what your trying to do, I believe your missing one of the most useful features of Hooks & React, and that's composition.

Here is an example of what you have done, but just creating another component called <IncrementButton/>, to me it just makes the code much easier to understand / debug. Custom hooks are great, but for doing this I believe it's the wrong tool..

const { useEffect, useState } = React;

const IncrementButton = props => {
  const {val, setVal, children} = props;
  return <button
    onClick={() => setVal(val + 1)}
  >{children}</button>;
}

const Home = () => {
  const [val, setVal] = useState(0);
  return (
    <div>
      <div>{val}</div>
      <IncrementButton val={val} setVal={setVal}>
        Click me
      </IncrementButton>
    </div>
  );
};

ReactDOM.render(<Home />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>

Upvotes: 0

Tholle
Tholle

Reputation: 112787

val and setVal will change on every render, which in turn will cause incrementOnClick to become a new function reference, and your useClick effect will always be invoked.

You could instead give a function as first argument to setVal. This function gets the current val as argument and returns the new value. This way incrementOnClick will always be the same function.

const { useEffect, useState, useCallback } = React;

function useClick(onClick, setVal, val) {
  useEffect(() => {
    console.log("Click");
    setVal(val + 1);
  }, [onClick]);
}

const Home = () => {
  const [val, setVal] = useState(0);
  const incrementOnClick = useCallback(() => {
    console.log("go set");
    setVal(val => val + 1);
  }, []);

  useClick(incrementOnClick, setVal, val);

  return (
    <div>
      <div>{val}</div>
      <button onClick={incrementOnClick}>Click me</button>
    </div>
  );
};

ReactDOM.render(<Home />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>

The code above shows how you could get away from the infinite loop and could be valuable for experimentation, but most of it isn't necessary. You could write the same functionality like this instead:

const { useState } = React;

const Home = () => {
  const [val, setVal] = useState(1);
  
  return (
    <div>
      <div>{val}</div>
      <button onClick={() => setVal(val + 1)}>Click me</button>
    </div>
  );
};

ReactDOM.render(<Home />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>

Upvotes: 9

Related Questions