2

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

mehdi parastar
  • 737
  • 4
  • 13
  • 29

5 Answers5

0

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.

hellogoodnight
  • 1,989
  • 7
  • 29
  • 57
  • thanks, your solution is good but when the steps are many, for each child component should I have a "state" in the parent component that makes the parent not well-formed. – mehdi parastar May 28 '21 at 09:26
  • True, but like I say you can still put the state in one single object if you like, and still only keep it in the parent. – hellogoodnight May 28 '21 at 09:30
0

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.

Surjeet Bhadauriya
  • 6,755
  • 3
  • 34
  • 52
  • thanks for your solution and description, but when I use inputValue instead of step, for each change in input, the parent state will be affected. I now only when the user clicked on next/prev button, the child state saved in the parent. – mehdi parastar May 28 '21 at 09:30
0

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]
  );
Zymka
  • 111
  • 1
  • 4
0

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

Bogy
  • 944
  • 14
  • 30
0

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

mehdi parastar
  • 737
  • 4
  • 13
  • 29