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 }
]);
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>
);
};