Search code examples
reactjsweb-frontendreact-functional-componentreact-state

Parent state getting reset back to initial value after different child is clicked


I have a parent and child components. Whenever the Link in the child component is clicked, I want to execute the parent's click handler that is passed in as props. In the click handler, I simply want to increment an integer state in the parent component.

The issue: the numCars state is correctly incremented for each single Link (child component) I click. For instance, if I click on the Link "car1" 5 times, the numCars state is incremented to 5. However, if I then click on "car2", the numCars state is reset back to 0. If I continue clicking "car2", numCars is incremented properly, until I click on another car. It's as if each child has its own parent component.

Why is this happening?

export default function Garage() {
   const [numCars, setNumCars] = useState(0);

   const carHandleClick = (carName) => {
      console.log("Clicked by " + carName); 
      setNumCars(numCars + 1);
   } 

   const cars = [
      {
         name: "car1"
      },
      {
         name: "car2"
      },
      {
         name: "car3"
      }
   ]

   return (
      <ul>
         {cars.map((car) =>
            <Car name={car.name} carHandleClick={carHandleClick}/>   
         }
      <ul>
   );
}

export default function Car( {name, carHandleClick} ) {
   return (
      <li>
         <Link to=... onClick={() => carHandleClick(name)}
            {name}
         </Link>
      </li>
   )
}

Solution

  • carHandleClick = (carName) => {
     setNumCars(prev => prev + 1);
    } 
    

    you want always to use the functional form of the state setter if you want to update it based on its previous value

    or a useCallback hook with [numCars]:

    carHandleClick = useCallback((carName) => {
    setNumCars(numCars + 1);
    },[numCars])
    

    Update: the issue was related to the fact navigate with the link unmounts and remounts the parent component so its state is reset.
    one way to handle this is to use a context in the top level:

    export const MyContext = createContext();
    
    const App = () => {
      const [numCars, setNumCars] = useState(0);
      const carHandleClick = (carName) => {
        console.log("Clicked by " + carName);
        setNumCars((prev) => prev + 1);
      };
    
      return (
        <MyContext.Provider value={{ numCars, carHandleClick }}>
          //...
        </MyContext.Provider>
      );
    }
    

    now you juste consume it from both parent and child:

    import { MyContext } from "./App";
    
    export default function Garage() {
      const { carHandleClick, numCars } = useContext(MyContext);
    
      //...
      return (
        <ul>
          {cars.map((car) => (
            <Car name={car.name} />
          ))}
        </ul>
      );
    }
    
    import { MyContext } from "./App";
    
    function Car({ name }) {
      const { carHandleClick, numCars } = useContext(MyContext);
      //...
      return (
        <li>
          <Link onClick={() => carHandleClick(name)}>{name}</Link>
        </li>
      );
    }
    

    Learn more about useContext