skip to Main Content

so i have a wrapper component with a button which produce its children component whenever clicked no matter how many times. it is a wrapper component and i want to use it how many times i need but everytime i switch from one to another its state will reset. i switch in this piece of code: (in summary there are divs when you click on each one of them they show the respective handler)

import { useState } from "react";
import "./../styles/left.css";
import rightIcon from "./../assets/right.svg";
import General from "./general";
import Educational from "./educational";
import Professional from "./professional";
import Handler from "./handler";


export default function Left({ data, onChange }) {
  const [selected, setSelected] = useState(0);
  return (
    <div className="forms">
      <div className="form-type">
        <div className="general-btn" onClick={() => setSelected(0)}>
          general information{" "}
          {selected === 0 ? <img src={rightIcon} alt="" /> : ""}{" "}
        </div>
        <div className="educational-btn" onClick={() => setSelected(1)}>
          educational experience{" "}
          {selected === 1 ? <img src={rightIcon} alt="" /> : ""}
        </div>
        <div className="practical-btn" onClick={() => setSelected(2)}>
          practical experience{" "}
          {selected === 2 ? <img src={rightIcon} alt="" /> : ""}
        </div>
      </div>
      <div className="form">
      {selected === 0 ? <General data={data} onChange={onChange} /> : ""}{" "}
      {selected === 1 ? <Handler> <Educational data={data} onChange={onChange}/> </Handler> : ""}{" "}
      {selected === 2 ? <Handler> <Professional data={data} onChange={onChange}/></Handler> : ""}{" "}
      </div>
    </div>
  );
}

and it is my handler component:

import { useState } from "react";

export default function Handler({children}){
    const [childrens, setChildrens] = useState([]);

  const addChild = () => {
    setChildrens(prevChildren => [...prevChildren, children]);
  };
    return(
        <div>
            {childrens}
            <button onClick={addChild}>add</button>
        </div>
    )
}

i thought the states are independent in each component but i see otherwise. why?

also i tried to lift my state up from my handler component but this way i have to repeat myself and i hate doing it.

2

Answers


  1. The part

    <div className="form">
      {selected === 0 && <General data={data} onChange={onChange} />}
      {selected === 1 && <Handler><Educational data={data} onChange={onChange} /></Handler>}
      {selected === 2 && <Handler><Professional data={data} onChange={onChange} /></Handler>}
    </div>
    

    is equivalent to

    <div className="form">
      {selected === 0
        ? <General data={data} onChange={onChange} />
        : (
          <Handler>
            {selected === 1
              ? <Educational data={data} onChange={onChange} />
              : <Professional data={data} onChange={onChange}/>
            }
          </Handler>
        )
      }
    </div>
    

    for all purposes – they render the same element tree. (I’ve ignored the empty/space strings, I don’t think they matter for React’s component reconciliation algorithm).

    So when you switch between options 1 and 2, there’s still a <Handler> element, and React renders the same component with its existing state. If that’s not what you expect, you should be able to fix this by giving the elements different keys:

    <div className="form">
      {selected === 0 && <General key={0} data={data} onChange={onChange} />}
      {selected === 1 && <Handler key={1}><Educational data={data} onChange={onChange} /></Handler>}
      {selected === 2 && <Handler key={2}><Professional data={data} onChange={onChange} /></Handler>}
    </div>
    

    You’ll probably have a similar problem later with the childrens inside Handler – when you copy the children into your array you’ll probably want to give it a unique id so that you can (re)move/reorder the childrens individually later; use cloneElement for that.

    Login or Signup to reply.
  2. The Handler component state resets because when the selected state updates the Handler components are conditionally rendered; only the active Handler is mounted, the other are unmounted. When Handler remounts it will start with the initial state.

    To address this you will need to lift the state up to a common ancestor component that remains mounted while these Handler components are conditionally rendered so their state can be "restored" when they remount.

    An example solution would be to store the children "counts" in state in the Left component.

    function Left({ data, onChange }) {
      const [selected, setSelected] = useState(0);
      const [counts, setCounts] = useState({});
    
      const addHandler = (id) =>
        setCounts((counts) => ({
          ...counts,
          [id]: (counts[id] ?? 0) + 1
        }));
    
      return (
        <div className="forms">
          <div className="form-type">
            <div className="general-btn" onClick={() => setSelected(0)}>
              general information
              {selected === 0 && <img src={rightIcon} alt="" />}
            </div>
            <div className="educational-btn" onClick={() => setSelected(1)}>
              educational experience
              {selected === 1 && <img src={rightIcon} alt="" />}
            </div>
            <div className="practical-btn" onClick={() => setSelected(2)}>
              practical experience
              {selected === 2 && <img src={rightIcon} alt="" />}
            </div>
          </div>
          <div className="form">
            {selected === 0 && <General data={data} onChange={onChange} />}
            {selected === 1 && (
              <Handler count={counts[1]} onAdd={() => addHandler(1)}>
                <Educational data={data} onChange={onChange} />
              </Handler>
            )}
            {selected === 2 && (
              <Handler count={counts[2]} onAdd={() => addHandler(2)}>
                <Professional data={data} onChange={onChange} />
              </Handler>
            )}
          </div>
        </div>
      );
    }
    

    Update Handler to initialize the childrens state to the number of saved children count, and call the onAdd callback to increment the count each time a new child is added.

    import { cloneElement, useState } from "react";
    import { nanoid } from "nanoid";
    
    function Handler({ children, count, onAdd }) {
      const [childrens, setChildrens] = useState(
        Array.from({ length: count }, () =>
          cloneElement(children, { key: nanoid() })
        )
      );
    
      const addChild = () => {
        onAdd();
        setChildrens((prevChildren) => [
          ...prevChildren,
          cloneElement(children, { key: nanoid() })
        ]);
      };
    
      return (
        <div>
          <button onClick={addChild}>add</button>
          {childrens}
        </div>
      );
    }
    

    Demo

    const rightIcon = "";
    
    const nanoid = () => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
    
    const General = () => <h1>General</h1>;
    const Educational = () => <h1>Educational</h1>;
    const Professional = () => <h1>Professional</h1>;
    
    function Handler({ children, count, onAdd }) {
      const [childrens, setChildrens] = React.useState(
        Array.from({ length: count }, () =>
          React.cloneElement(children, { key: nanoid() })
        )
      );
    
      const addChild = () => {
        onAdd();
        setChildrens((prevChildren) => [
          ...prevChildren,
          React.cloneElement(children, { key: nanoid() })
        ]);
      };
    
      return (
        <div>
          <button onClick={addChild}>add</button>
          {childrens}
        </div>
      );
    }
    
    function Left({ data, onChange }) {
      const [selected, setSelected] = React.useState(0);
      const [counts, setCounts] = React.useState({});
    
      const addHandler = (id) =>
        setCounts((counts) => ({
          ...counts,
          [id]: (counts[id] || 0) + 1
        }));
    
      return (
        <div className="forms">
          <div className="form-type">
            <div className="general-btn" onClick={() => setSelected(0)}>
              general information
              {selected === 0 && <img src={rightIcon} alt="" />}
            </div>
            <div className="educational-btn" onClick={() => setSelected(1)}>
              educational experience
              {selected === 1 && <img src={rightIcon} alt="" />}
            </div>
            <div className="practical-btn" onClick={() => setSelected(2)}>
              practical experience
              {selected === 2 && <img src={rightIcon} alt="" />}
            </div>
          </div>
          <div className="form">
            {selected === 0 && <General data={data} onChange={onChange} />}
            {selected === 1 && (
              <Handler count={counts[1]} onAdd={() => addHandler(1)}>
                <Educational data={data} onChange={onChange} />
              </Handler>
            )}
            {selected === 2 && (
              <Handler count={counts[2]} onAdd={() => addHandler(2)}>
                <Professional data={data} onChange={onChange} />
              </Handler>
            )}
          </div>
        </div>
      );
    }
    
    const rootElement = document.getElementById("root");
    const root = ReactDOM.createRoot(rootElement);
    
    root.render(
      <React.StrictMode>
        <Left />
      </React.StrictMode>
    );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
    <div id="root" />
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search