Search code examples
javascriptreactjsreact-hooksreact-component

Reactjs: How to access state of child component.from a method in parent component that depends on state of child component


I need to access a method handleCancelEdit() defined in parent component. But, the matter here is that every child component will have its own cancelEdit state. Now, what is happening is, if I call handleCancelEdit() from one child component, every other of the same child components is taking the state and updating themselves(the method is not completely defined yet). That's why, I have defined the cancelEdit state in the child component, thinking that it belongs to this child component only.

Now, how do I make the handleCancelEdit() method make changes to the only child component which called it?

The parent:

function Parent() {
    const handleCancelEdit = () => {
        setCancelEdit(!cancelEdit);  // defined in child component
        setEdit(!edit);       // defined in child component
        ...
    };
    return (
    <div>
        <ChildComponent
            fieldName={"Email"}
            value={email}
            inputType={"text"}
            placeHolder={"Enter email"}
            name={"email"}
            on_change={(e)=>setEmail(e.target.value)}
            on_click={handleUserEmail}
         />
         <ChildComponent
             fieldName={"About"}
             value={about}
             inputType={"text"}
             placeHolder={"Enter some details about yourself"}
             name={"about"}
             on_change={(e)=>setAbout(e.target.value)}
             on_click={handleUserAbout}
         />
    </div>
    );
}

Child component:

function ChildComponent({fieldName, value, inputType, placeHolder, name, on_change, on_click}) {
    const [ edit, setEdit ] = useState(false);
    const [ cancelEdit, setCancelEdit ] = useState(false)
    const isEdit = edit;
    return (
        <p>{fieldName}: {value === ''? (
            <span>
                <input type={inputType} placeholder={placeHolder}
                    name={name}  onChange={on_change}
                />
                <button type="submit" onClick={on_click}>Add</button>
            </span>
            ) : ( !isEdit ? (<span> {value} <button onClick={e=>setEdit(!edit)}>Edit</button></span>) :
            (<span>
                <input type={inputType} value={value}
                        name={name}  onChange={on_change}
                />
                <button type="submit" onClick={on_click}>Save</button>
                <button type="submit" onClick={handleCancelEdit}>Cancel</button>
            </span>)                            
            )}
        </p>
    );
};

I hope it could make it understandable that one child component should not make others to update. Now, how do I do it in this scenario?

EDIT

After making changes according to Linda Paiste: gif of error

The input field in the child component is not working even though the onChange in both parent and child is correct!


Solution

  • It is always more logical to pass state and data down rather than up. If the Parent needs to interact with the edit state then that state should live in the parent. Of course we want independent edit states for each child, so the parent can't just have one boolean. It needs a boolean for each child. I recommend an object keyed by the name property of the field.

    In ChildComponent, we move isEdit and setEdit to props. handleCancelEdit is just () => setEdit(false) (does it also need to clear the state set by onChange?).

    
    function ChildComponent({fieldName, value, inputType, placeHolder, name, onChange, onSubmit, isEdit, setEdit}) {
        return (
            <p>{fieldName}: {value === ''? (
                <span>
                    <input type={inputType} placeholder={placeHolder}
                        name={name}  onChange={onChange}
                    />
                    <button type="submit" onClick={onSubmit}>Add</button>
                </span>
                ) : ( !isEdit ? (<span> {value} <button onClick={() =>setEdit(true)}>Edit</button></span>) :
                (<span>
                    <input type={inputType} value={value}
                            name={name}  onChange={onChange}
                    />
                    <button type="submit" onClick={onSubmit}>Save</button>
                    <button type="submit" onClick={() => setEdit(false)}>Cancel</button>
                </span>)                            
                )}
            </p>
        );
    };
    

    In Parent, we need to store those isEdit states and also create a setEdit function for each field.

    function Parent() {
      const [isEditFields, setIsEditFields] = useState({});
    
      const handleSetEdit = (name, isEdit) => {
        setIsEditFields((prev) => ({
          ...prev,
          [name]: isEdit
        }));
      };
    
      /* ... */
    
      return (
        <div>
          <ChildComponent
            fieldName={"Email"}
            value={email}
            inputType={"text"}
            placeHolder={"Enter email"}
            name={"email"}
            onChange={(e) => setEmail(e.target.value)}
            onSubmit={handleUserEmail}
            isEdit={isEditFields.email}
            setEdit={(isEdit) => handleSetEdit("email", isEdit)}
          />
          <ChildComponent
            fieldName={"About"}
            value={about}
            inputType={"text"}
            placeHolder={"Enter some details about yourself"}
            name={"about"}
            onChange={(e) => setAbout(e.target.value)}
            onSubmit={handleUserAbout}
            isEdit={isEditFields.about}
            setEdit={(isEdit) => handleSetEdit("about", isEdit)}
          />
        </div>
      );
    }
    

    You can clean up a lot of repeated code by storing the values as a single state rather than individual useState hooks. Now 5 of the props can be generated automatically just from the name.

    function Parent() {
      const [isEditFields, setIsEditFields] = useState({});
    
      const [values, setValues] = useState({
          about: '',
          email: ''
      });
    
      const getProps = (name) => ({
          name,
          value: values[name],
          onChange: (e) => setValues(prev => ({
              ...prev,
              [name]: e.target.value
          })),
          isEdit: isEditFields[name],
          setEdit: (isEdit) => setIsEditFields(prev => ({
              ...prev,
              [name]: isEdit
          }))
      });
    
      const handleUserEmail = console.log // placeholder
      const handleUserAbout = console.log // placeholder
    
      return (
        <div>
          <ChildComponent
            fieldName={"Email"}
            inputType={"text"}
            placeHolder={"Enter email"}
            onSubmit={handleUserEmail}
            {...getProps("email")}
          />
          <ChildComponent
            fieldName={"About"}
            inputType={"text"}
            placeHolder={"Enter some details about yourself"}
            onSubmit={handleUserAbout}
            {...getProps("about")}
          />
        </div>
      );
    }