Search code examples
reactjscomponentsrenderingnext.jsuse-state

useState passed from parent to child and used in child doesn't trigger the re-render of the child


So, I guess the parent doesn't re-render the child component when the useState is updated. But the component should re-render automatically when using useState right ? Or I am missing something...

index.js

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

import Clicking from "./clicking";

export default function IndexPage() {
  const parentBtn = useRef(null);

  const [clicks, setClicks] = useState(0);

  useEffect(() => {
    console.log("Clicks: ", clicks);

    if (null !== parentBtn.current) {
      parentBtn.current.addEventListener("click", () => setClicks(clicks + 1));
    }
  }, [clicks]);

  return (
    <div>
      <p>Clicks number : {clicks}</p>
      <Clicking clicks={clicks} setClicks={setClicks} />
      <button ref={parentBtn}>Parent button</button>
    </div>
  );
}

clicking.js

import { useEffect, useRef } from "react";

const Clicking = ({ clicks, setClicks }) => {
  const childBtn = useRef(null);

  useEffect(() => {
    childBtn.current.addEventListener("click", () => setClicks(clicks + 1));
  }, []);

  return <button ref={childBtn}>Child button</button>;
};

export default Clicking;

Does anyone know how to fix that ?

Here a codesandbox of the case : https://codesandbox.io/s/pensive-sun-9993d

Thanks for the help !


Solution

  • The issue is that your useEffect only runs once, and therefore only receives the initial value of clicks.
    What you need to do is stop passing the clicks value to the child and simply get the state value from the setClicks function, as your updates depends on the previous state value anyway.

    You should also pass setClicks as a dependency to useEffect, even though it is not technically mandatory in this case as the function is memoized and therefore will never change, but it's a best practice and you will avoid bugs in the future if you do it.

    However the useEffect in the child is useless in your case anyway and you would be better off simply passing a onClick prop to the child that will trigger the parent's function to update the counter when the button is clicked.
    The same thing is true for the parent though, and you could simply use the onClick prop instead of using useRef and end up with this much simpler code:

    index.js

    import { useState } from "react";
    
    import Clicking from "./clicking";
    
    export default function IndexPage() {
      const [clicks, setClicks] = useState(0);
    
      const updateClicksCounter = () => {
        setClicks(clicks => clicks + 1);
      };
    
      return (
        <div>
          <p>Clicks number : {clicks}</p>
          <Clicking onClick={updateClicksCounter} />
          <button onClick={updateClicksCounter}>Parent button</button>
        </div>
      );
    }
    

    clicking.js

    const Clicking = ({ onClick }) => {
      return <button onClick={onClick}>Child button</button>;
    };
    
    export default Clicking;