In ReactJS How to save Child State in Parent state on destroying child?

Viewed 61

I have a parent Component and two child Component as below:

ParentComponent:

import React, { useState } from "react";
import { Child1Comp } from "./Child1Comp";
import { Child2Comp } from "./Child2Comp";

export function ParentComp() {
  const [step, setStep] = useState(1);
  const [parentState, setParentState] = useState({});
  const handleNextBtn = () => setStep((step) => step + 1);
  const handlePrevBtn = () => setStep((step) => step - 1);

  return (
    <div>
      <div>{`the parent state is: ${JSON.stringify(parentState)}`}</div>
      {step === 1 && (
        <Child1Comp
          step={step}
          parentState={parentState}
          setParentState={setParentState}
        />
      )}
      {step === 2 && (
        <Child2Comp
          step={step}
          parentState={parentState}
          setParentState={setParentState}
        />
      )}
      <br />
      <button
        id="nextBtn"
        name="nextBtn"
        onClick={handleNextBtn}
        disabled={step === 2}
      >
        Next
      </button>
      <button
        id="nextBtn"
        name="nextBtn"
        onClick={handlePrevBtn}
        disabled={step === 1}
      >
        Prev
      </button>
      <br />
      <br />
      {`current step is : ${step}`}
    </div>
  );
}

Child1Component:

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

export function Child1Comp({ step, parentState, setParentState }) {
  const [inputValue, setInputValue] = useState();
  const handleChange = (e) => setInputValue(e.target.value);

  useEffect(() => setInputValue(parentState.child1), []);
  useEffect(
    () =>
      setParentState((parentState) => ({
        ...parentState,
        child1: inputValue
      })),
    [step]
  );

  return (
    <div>
      <br />
      name:
      <input
        id="child1"
        name="child1"
        value={inputValue}
        onChange={handleChange}
      />
    </div>
  );
}

Child2Component:

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

export function Child2Comp({ step, parentState, setParentState }) {
  const [inputValue, setInputValue] = useState();
  const handleChange = (e) => setInputValue(e.target.value);

  useEffect(() => setInputValue(parentState.child2), []);
  useEffect(
    () =>
      setParentState((parentState) => ({
        ...parentState,
        child2: inputValue
      })),
    [step]
  );

  return (
    <div>
      <br />
      family:
      <input
        id="child2"
        name="child2"
        value={inputValue}
        onChange={handleChange}
      />
    </div>
  );
}
  • In the parent Component, I have a step state for determining which child should be shown.
  • Each child has itself state.

now I want when the user changes the step value by clicking on the Next button, the child state value save in the parent state, so I use this code in each child:

child1:

useEffect(
        () =>
          setParentState((parentState) => ({
            ...parentState,
            child1: inputValue
          })),
        [step]
      );

child2:

useEffect(
        () =>
          setParentState((parentState) => ({
            ...parentState,
            child2: inputValue
          })),
        [step]
      );

but when the step state changes in the parent Component, none of the top useEffect runs. and child state not save in parent state.:(

Do you have any idea to solve this problem?

codesandbox link

5 Answers

Keep the state in the parent component and pass down getters and setters as props. Something like this:

export function ParentComp() {
    const [step, setStep] = useState(1);
    const [child1Input, setChild1Input] = useState(null);
    const [child2Input, setChild2Input] = useState(null);
    
    return (
      <div>
      
        {step === 1 && (
          <Child1Comp
            input={child1Input}
            setInput={setChild1Input}
          />
        )}
        {step === 2 && (
          <Child1Comp
          input={child2Input}
          setInput={setChild2Input}
        />
        )}

        ...

Now the child components don't need no state variables or useEffects anymore, which means less risk of bugs and easier to understand the flow once your code grows.

export function Child1Comp({ input, setInput }) {

 return (
    <div>
      <br />
      name:
      <input
        id="child1"
        name="child1"
        value={inputValue}
        onChange={e => setInput(e.target.value)}
      />
    </div>
  );
}

Of course, you can still keep the state in an object if you like, I hope you get the general idea though.

Solution:

Keep watch on inputValue instead of step in useEffect

Child1

useEffect(
    () =>
      setParentState((parentState) => ({
        ...parentState,
        child1: inputValue
      })),
    [inputValue]
  );

Child2

useEffect(
    () =>
      setParentState((parentState) => ({
        ...parentState,
        child2: inputValue
      })),
    [inputValue]
  );

Problem:

When you click on next/prev, the step value changes to 1 or 2 respectively in the parent. And accordingly, the respective child components get hidden and no longer exist on DOM. So that's why your useEffect code does not execute.

The child component dismounts when the step changes and it cannot see when the step prop changes. Instead, you should watch the inputValue and set it to parent every time it changes.

  useEffect(
    () =>
      setParentState((parentState) => ({
        ...parentState,
        child2: inputValue
      })),
    [inputValue]
  );

If you really want to use useEffect, there are missing dependencies in your useEffect (second parameter of useEffect, dependencies are in an array):

  useEffect(
    () => setInputValue(parentState.child2), 
   [parentState.child2] //If parentState.child2 change, useEffect function will be called
  );
  
  useEffect(
    () =>
      setParentState((parentState) => ({
        ...parentState,
        child2: inputValue
      })),
    [step, inputValue, setParentState] //If one of these variables change, useEffect function will be called
  );

Working example here.

But a better way to do it is to handle changes like this:

import React from "react";

export function Child1Comp({ parentState, setParentState }) {
  const handleChange = (e) =>
    setParentState((oldParentValues) => ({
      ...oldParentValues, //Spread old parent states
      [e.target.name]: e.target.value // override child1 input value here
    }));

  return (
    <div>
      <br />
      name:
      <input
        id="child1"
        name="child1"
        value={parentState.child1}
        onChange={handleChange}
      />
    </div>
  );
}

Woking example here

Note: labels (name, familly, ...), and names (child1, child2, ...) are redundants and could be passed as props, in order to have a single ChildComponent. Like this demo

I finally use this solution:

i add saveStep state to parent component that is object of 'step,req,conf,action':

  • step=> current step number.
  • req=>bool value that show request to change step.
  • conf=> bool value that show child state saved in parent or no.
  • action=>next or prev button clicked.

step only when changed that req and conf both value is true.

ParentComponent:

import React, { useEffect, useState } from "react";
import { Child1Comp } from "./Child1Comp";
import { Child2Comp } from "./Child2Comp";

export function ParentComp() {
  const [parentState, setParentState] = useState({});
  const [saveStep, setSaveStep] = useState({
    step: 1,
    req: false,
    conf: false,
    action: ""
  });

  useEffect(() => {
    if (saveStep.req && saveStep.conf) {
      setSaveStep((saveStep) => ({
        step:
          saveStep.action === "next"
            ? saveStep.step + 1
            : saveStep.action === "prev"
            ? saveStep.step - 1
            : saveStep.step,
        req: false,
        conf: false
      }));
    }
  }, [saveStep]);

  const handleNextBtn = () =>
    setSaveStep((saveStep) => ({
      ...saveStep,
      req: true,
      action: "next"
    }));

  const handlePrevBtn = () =>
    setSaveStep((saveStep) => ({
      ...saveStep,
      req: true,
      action: "prev"
    }));

  return (
    <div>
      <div>{`the parent state is: ${JSON.stringify(parentState)}`}</div>
      {saveStep.step === 1 && (
        <Child1Comp
          saveStep={saveStep}
          setSaveStep={setSaveStep}
          parentState={parentState}
          setParentState={setParentState}
        />
      )}
      {saveStep.step === 2 && (
        <Child2Comp
          saveStep={saveStep}
          setSaveStep={setSaveStep}
          parentState={parentState}
          setParentState={setParentState}
        />
      )}
      <br />
      <button
        id="nextBtn"
        name="nextBtn"
        onClick={handleNextBtn}
        disabled={saveStep.step === 2}
      >
        Next
      </button>
      <button
        id="nextBtn"
        name="nextBtn"
        onClick={handlePrevBtn}
        disabled={saveStep.step === 1}
      >
        Prev
      </button>
      <br />
      <br />
      {`current step is : ${saveStep.step}`}
    </div>
  );
}

Child1Component:

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

export function Child1Comp({
  saveStep,
  setSaveStep,
  parentState,
  setParentState
}) {
  const [inputValue, setInputValue] = useState();
  const handleChange = (e) => setInputValue(e.target.value);

  useEffect(() => setInputValue(parentState.child1), []);
  useEffect(() => {
    if (saveStep.req) {
      setParentState((parentState) => ({
        ...parentState,
        child1: inputValue
      }));
      setSaveStep((saveStep) => ({ ...saveStep, conf: true }));
    }
  }, [saveStep.req]);

  return (
    <div>
      <br />
      child1:
      <input
        id="child1"
        name="child1"
        value={inputValue}
        onChange={handleChange}
      />
    </div>
  );
}

Child2Component:

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

export function Child2Comp({
  saveStep,
  setSaveStep,
  parentState,
  setParentState
}) {
  const [inputValue, setInputValue] = useState();
  const handleChange = (e) => setInputValue(e.target.value);

  useEffect(() => setInputValue(parentState.child2), []);
  useEffect(() => {
    if (saveStep.req) {
      setParentState((parentState) => ({
        ...parentState,
        child2: inputValue
      }));
      setSaveStep((saveStep) => ({ ...saveStep, conf: true }));
    }
  }, [saveStep.req]);

  return (
    <div>
      <br />
      child2:
      <input
        id="child2"
        name="child2"
        value={inputValue}
        onChange={handleChange}
      />
    </div>
  );
}

codesandbox Link

Related