Home > Back-end >  React State and Events managed by Child Components
React State and Events managed by Child Components

Time:10-21

Child component is managing the state of parent objects using callback function. The code blow works well with just one variable but gives and error while dealing with Objects. The error I get is while entering values to the textarea..

remarks.map is not a function

Please help me out with this problem.

Requirement is : Parent has 5 objects. So, there are 5 child Form components. One such Form component is Remarks component where two text-area are being displayed. Hence, I tried to map remarks object and display two text areas in the child Component. So, on load, the Parent will pass objects with/without data to all the child components. As this is a form so users may want to edit the contents. So, I am trying to manage the state and events in the child component and pass it back to the parent component. Parent component saves all 5 objects on click of save button.

Also please do let me know if Ref here is of any use. Thank you.

App.jsx

export default function App() {
  const [remarks, setRemarks] = useState( [{ id: 1, remarkVal: "hello man" }]);

  const setterRemarks = useCallback(
    (val) => {
      setRemarks(val);
    },
    [setRemarks]
  );

  return (
    <div className="App">
      <RemarksPage
        remarks={remarks}
        setterRemarks={setterRemarks}
        placeholder="I am remarks section..."
      />
    </div>
  );
}

RemarksPage.tsx

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

export default ({ remarks, placeholder, setterRemarks, ...rest }: any) => {
  const childRef = useRef();
  const [childState, setChildState] = useState(remarks);

  useEffect(() => {
    setterRemarks(childState);
  }, [setterRemarks, childState]);

  const onSliderChangeHandler = (e: any) => {
    remarks.map((items: any) => {
      if (items?.id == e?.target?.id) {
        setChildState((prevState: any) => ({
          ...prevState,
          [e.target.name]: e.target.value
        }));
      }
    });
  };

  return (
    <div className="container">
      {remarks?.map((items: any) => {
        return (
          <div key={items?.id}>
            <label>
              <textarea
                name="remarkVal"
                id={items?.id}
                onChange={(e) => onSliderChangeHandler(e)}
                value={items?.remarksVal}
                ref={childRef}
                placeholder={placeholder}
              />
            </label>
          </div>
        );
      })}
    </div>
  );
};

Getting a new row on edit the code as per answers.

 setChildState((prevState: any) => [
          ...prevState,
          { [e.target.name]: e.target.value }
        ]);

enter image description here

enter image description here

CodePudding user response:

Your state value is an array:

const [remarks, setRemarks] = useState( [{ id: 1, remarkVal: "hello man" }]);

When you update state here, you change it to an object:

setChildState((prevState: any) => ({
  ...prevState,
  [e.target.name]: e.target.value
}));

As the error states, map() is not a function on objects. Keep your state value as an array. For example, you can append an element to it:

setChildState((prevState: any) => ([
  ...prevState,
  e.target.value
]));

Or perhaps modify (replace) the single item within the array:

setChildState((prevState: any) => ([{
  ...prevState[0],
  [e.target.name]: e.target.value
}]));

(Note: This assumes the array will always have exactly one item. Though you seem to be making that assumption anyway. And it's pretty strange to maintain an array which will only ever have one item.)

Or maybe the array will contain multiple items, and you want to update one specific one? Your weird use of .map() inside of onSliderChangeHandler may be trying to imply that. In that case you might want something like this:

const onSliderChangeHandler = (e: any) => {
  setChildState((prevState: any) => ([
    ...prevState.map(p => {
      if (p.id === e.target.id) {
        return { ...p, [e.target.name]: e.target.value };
      } else {
        return p;
      }
    });
  ]));
};

Note how, instead of mapping over the array to update state to only one item, state is updated to the resulting array of the map operation, in which a target item is replaced (and all others returned as-is).


Basically, what you need to do is take a step back and examine/understand the data structure you are using. Should it be an object or an array of objects? Why? When an update is made, what should be changed? Why? Don't just make random changes to "get it to work", deliberately maintain the data you want to maintain.

CodePudding user response:

By Konrad Linkowski Link : https://codesandbox.io/s/relaxed-fog-8nfnhb?file=/src/App.js

export default function App() {
  const [Form, setForm] = useState(Details);
  const [remarks, setRemarks] = useState([{ id: 1, remarksVal: "hello man" }]);
  const [parentState, setParentState] = useState(123);
  // make wrapper function to give child
  const wrapperSetParentState = useCallback(
    (val) => {
      setParentState(val);
    },
    [setParentState]
  );
  const setterRemarks = useCallback(
    (val) => {
      setRemarks(val);
    },
    [setRemarks]
  );

  useEffect(() => {
    console.log("parent", remarks);
  }, [remarks]);

  return (
    <div className="App">
      <RemarksPage
        remarks={remarks}
        setterRemarks={setterRemarks}
        placeholder="I am remarks section..."
      />
      <div style={{ margin: 30 }}>
        <Child
          parentState={parentState}
          parentStateSetter={wrapperSetParentState}
        />
        <br />
        {parentState}
      </div>
    </div>
  );
}

import { Key, useEffect, useRef, useState } from "react";

export default ({ remarks, placeholder, setterRemarks, ...rest }) => {
  const childRef = useRef();
  const [childState, setChildState] = useState(remarks);

  useEffect(() => {
    setterRemarks(childState);
  }, [setterRemarks, childState]);

  const onSliderChangeHandler = (id: Key | null | undefined, value: string) => {
    setChildState((remarks: any[]) =>
      remarks.map((remark) =>
        remark.id === id ? { ...remark, remarksVal: value } : remark
      )
    );
  };

  return (
    <div className="container">
      {remarks?.map(
        (items: {
          id: Key | null | undefined;
          remarksVal: string | number | readonly string[] | undefined;
        }) => {
          return (
            <div key={items?.id}>
              <label>
                <textarea
                  name="remarkVal"
                  id={items?.id}
                  onChange={(e) =>
                    onSliderChangeHandler(items.id, e.target.value)
                  }
                  value={items?.remarksVal}
                  ref={childRef}
                  placeholder={placeholder}
                />
              </label>
            </div>
          );
        }
      )}
    </div>
  );
};

  • Related