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.
Also please do let me know if Ref here is of any use. Thank you.
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 }
]);
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.
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
Update
This maybe a Formik bug, and I have switched to https://react-hook-form.com, as Formik has not been updated for a while.
https://github.com/jaredpalmer/formik/issues/3716
Context
I'm using React, Formik, and google-map-react to allow store owner edit their store address with google map place autocomplete.
I have three components:
EditStoreInfoPage is the page component, which contains EditStoreInfoForm.
EditStoreInfoForm is the form component, which contains FormikAddressField. I uses Formik here.
FormikAddressField is the one form field that supports google place autocomplete.
Store information will be fetched from backend in EditStoreInfoPage, and passed down to EditStoreInfoForm and FormikAddressField. Whenever a new address is typed in FormikAddressField, it calls a callback function handleStoreLocationUpdate passed down from EditStoreInfoPage.
Issue
Render the page without any issue. I see that formValues are populated corrected with the data fetched from backend.
However, once I finished typing the address, the form get cleared except the store address is still there.
From the console output of above screenshot, I can see that function handleStoreLocationUpdate get called, however, console.log(formValues); in function handleStoreLocationUpdate of EditStoreInfoPage contains empty value for store fields. I was expecting that the formValues here still kept the value fetched from backend, not sure why these values get wiped out as I use React useState.
Any idea what went wrong?
Code
EditStoreInfoPage
This is the React component that first call backend API to get the store information based on storeIdentifier. formValues will be populated with these information, as you can see that setFormValues is being called. formValues is passed down to child component EditStoreInfoForm as props.
type EditStoreInfoPageProps = {
storeIdentifier: string;
};
const EditStoreInfoPage = (props: EditStoreInfoPageProps) => {
let navigate = useNavigate();
const [formValues, setFormValues] = React.useState<StoreAttributes>({
storeName: "",
storeLocation: "",
storeLocationLongitude: 0,
storeLocationLatitude: 0,
});
// Get store info.
React.useEffect(() => {
const user: CognitoUser | null = getCurrentBusinessAccountUser();
if (!user) {
Toast("Store Not Found!", "Failed to get store information!", "danger");
} else {
const storeIdentifier: string = user?.getUsername();
getStoreInfo(storeIdentifier)
.then((response) => {
setFormValues({
storeName: response?.storeName || "",
storeLocation: response?.storeLocation || "",
storeLocationLatitude: response?.storeLocationLatitude!,
storeLocationLongitude: response?.storeLocationLongitude!,
});
})
.catch((error) =>
Toast(
"Store Not Found!",
"Failed to get store information!",
"danger"
)
);
}
}, []);
const handleStoreLocationUpdate = (newStoreLocation: string) => {
const geocoder = new window.google.maps.Geocoder();
console.log("handleStoreLocationUpdate");
console.log(newStoreLocation);
console.log(formValues);
const geocodeRequest = { address: newStoreLocation };
const geocodeCallback = (
results: google.maps.GeocoderResult[] | null,
status: google.maps.GeocoderStatus
) => {
if (status === "OK") {
if (results && results[0]) {
const formValuesClone: StoreAttributes = structuredClone(formValues);
formValuesClone.storeLocation = newStoreLocation;
formValuesClone.storeLocationLatitude =
results[0].geometry.location.lat();
formValuesClone.storeLocationLongitude =
results[0].geometry.location.lng();
setFormValues(formValuesClone);
} else {
Toast("Not valid address!", "Please input a valid address", "danger");
}
} else {
Toast("Not valid address!", "Please input a valid address", "danger");
}
};
geocoder.geocode(geocodeRequest, geocodeCallback);
};
const handleSubmit = (data: StoreAttributes) => {
updateStore(props.storeIdentifier, JSON.stringify(data, null, 2))
.then((response) => {
if (response.status == 200) {
Toast(
"Updated!",
"The store information has been updated. Redirect to store page...",
"success"
);
navigate("/stores/" + props.storeIdentifier);
} else {
Toast(
"Updated failed!",
"Failed to update store information.",
"danger"
);
}
})
.catch((error) => {
Toast("Updated failed!!", error.message, "danger");
});
};
const handleUpdate = (data: StoreAttributes) => {
// make a deep clone here, as formValues here is an object.
console.log("handleUpdate");
const copy = structuredClone(data);
setFormValues(copy);
};
return (
<EditStoreInfoForm
formValues={formValues}
handleStoreLocationUpdate={handleStoreLocationUpdate}
handleUpdate={handleUpdate}
handleSubmit={handleSubmit}
/>
);
};
export default EditStoreInfoPage;
EditStoreInfoForm
EditStoreInfoForm is the form component. I use Formik here. It renders the form with props.formValues. It contains a child component FormikAddressField which will be used to support google place auto complete.
export type EditStoreInfoFormProps = {
formValues: StoreAttributes;
handleStoreLocationUpdate: any;
handleUpdate: any;
handleSubmit: any;
};
const EditStoreInfoForm = (props: EditStoreInfoFormProps) => {
console.log("EditStoreInfoForm");
const onBlur = () => {
console.log(props.formValues);
}
return (
<div className="flex justify-center items-center">
<Formik.Formik
initialValues={props.formValues}
enableReinitialize={true}
validationSchema={validationSchema}
validateOnChange={false}
validateOnBlur={false}
onSubmit={(values) => {
props.handleSubmit(values);
}}
>
{({ }) => (
<Formik.Form className="w-1/3">
<div className="form-group">
<div>
<FormikTextField
label="Store Name"
name="storeName"
placeholder={props.formValues?.storeName}
/>
</div>
<div className="form-group">
<FormikAddressField
label="Store Location"
name="storeLocation"
onAddressUpdate={props.handleStoreLocationUpdate}
placeholder={props.formValues?.storeLocation}
/>
</div>
<div className="w-full h-60">
{/* <GoogleMapLocationPin latitude={10} longitude={10} text="store"/> */}
<StoresGoogleMapLocation
googleMapCenter={{
lat: props.formValues.storeLocationLatitude,
lng: props.formValues.storeLocationLongitude,
}}
storeAddress={props.formValues?.storeLocation}
storeLocationLongitude={
props.formValues?.storeLocationLongitude
}
storeLocationLatitude={props.formValues?.storeLocationLatitude}
/>
</div>
<div className="form-group">
<button type="submit" className="form-button m-2 w-20 h-10">
Update
</button>
</div>
</Formik.Form>
)}
</Formik.Formik>
</div>
);
};
export default EditStoreInfoForm;
FormikAddressField
FormikAddressField is the field for autocomplete. See https://developers.google.com/maps/documentation/javascript/place-autocomplete to know what it is.
const FormikAddressField = ({ label, onAddressUpdate, ...props }: any) => {
const [field, meta] = useField(props);
const loader = new Loader({
apiKey: process.env.REACT_APP_GOOGLE_MAP_API_KEY!,
libraries: ["places", "geometry"],
});
const locationInputId = "locationInputId";
let searchInput: HTMLInputElement;
const autoCompleteInstanceRef = React.useRef<any>(null);
React.useEffect(() => {
loader.load().then(() => {
let searchInput = document.getElementById(
locationInputId
) as HTMLInputElement;
//console.log(searchInput);
autoCompleteInstanceRef.current = new google.maps.places.Autocomplete(
searchInput!,
{
// restrict your search to a specific type of resultmap
//types: ["address"],
// restrict your search to a specific country, or an array of countries
// componentRestrictions: { country: ['gb', 'us'] },
}
);
autoCompleteInstanceRef.current.addListener(
"place_changed",
onPlaceChanged
);
});
// returned function will be called on component unmount
return () => {
google.maps.event.clearInstanceListeners(searchInput!);
};
}, []);
const onPlaceChanged = () => {
const place: google.maps.places.PlaceResult =
autoCompleteInstanceRef.current.getPlace();
if (!place) return;
onAddressUpdate(place.formatted_address);
};
return (
<>
<label htmlFor={props.id || props.name} className="form-label">
{label}
</label>
<Field
id={locationInputId}
className="text-md w-full h-full m-0 p-0"
type="text"
{...field}
{...props}
/>
{meta.touched && meta.error ? (
<div className="error">{meta.error}</div>
) : null}
</>
);
};
export default FormikAddressField;
CodeSandbox
Here is a simplified version: https://nv1m89.csb.app/
The EditStoreInfoPage is above the EditStoreInfoForm. The formikValues in EditStoreInfoPage appears to be a copy, which is not updated every time the actual real-time formik values in EditStoreInfoForm are changed. Your real problem here is that you shouldn't have the clone in the first place.
Just pass the real store values up to the handler:
<FormikAddressField
label="Store Location"
name="storeLocation"
onAddressUpdate={(newAddress) => props.handleStoreLocationUpdate(newAddress, formValues)}
placeholder={props.formValues?.storeLocation}
/>
Now change:
const handleStoreLocationUpdate = (newStoreLocation: string) => {
To:
const handleStoreLocationUpdate = (newStoreLocation: string, formValues: StoreAttributes) => {
And use that argument.
As mentioned there are other issues here. Really you should refactor to get rid of this completely:
const [formValues, setFormValues] = React.useState<StoreAttributes>({
storeName: "",
storeLocation: "",
storeLocationLongitude: 0,
storeLocationLatitude: 0,
});
You'd do it by making the actual form state accessible to that component. Probably by changing to the useFormik pattern and loading that hook in the parent.
I want to create a function that will color the hearts when clicked.
I wrote a function that prints out elements for me, but when I click on any heart, it colors them all.
Where could the problem be?
My code:
const \[userInput, setUserInput\] = useState("");
const \[list, setList\] = useState(\[\]);
const \[hearth, setHearth\] = useState(false);
const \[active, setActive\] = useState(-1);
const handleChange = (e) =\> {
e.preventDefault();
setUserInput(e.target.value);
};
const handleSubmit = (e) =\> {
e.preventDefault();
setList(\[userInput, ...list\]);
setUserInput("");
};
const wishList = (e) =\> {
setHearth(!hearth);
};
useEffect(() =\> {}, \[userInput, list\]);
return (
\<div className="favMusic"\>
<h1>FavMusicList</h1>
\<form\>
\<input value={userInput} onChange={handleChange} type="text" /\>
\<button onClick={handleSubmit}\>Submit\</button\>
\</form\>
<ul className="favMusic__list">
{list.map((i, idx) => {
console.log(idx);
return (
<li key={idx}>
{i}{" "}
<div
id={idx}
onClick={() => wishList(idx)}
className={"hearth" + " " + (hearth ? "true" : "false")}>
<AiOutlineHeart
/>
</div>
</li>
);
})}
</ul>
</div>
I have tried all possible ways from setState to others found on the net but I have no idea how to solve it
Here's a working demo.
Assuming your state data is an array of items, each with its own boolean property indicating whether it's been "liked" by the user:
[
{
id: 1,
liked: true,
title: 'ListItem 1',
},
{
id: 2,
liked: false,
title: 'ListItem 2',
},
// ...
]
Then in your click handler, you'd want to loop over each of the objects to find the item with the corresponding id to change just the boolean property for that one item. For example:
const handleClick = (id) => {
const newLikes = items.map((item) => {
// check the current element's id against the
// id passed to the handler
if (item.id === id) {
// if it matches, update the liked property
// and return the modified object
return { ...item, liked: !item.liked };
}
// if it doesn't match, just return the
// original object
return item;
});
// update state with the new data
setItems(newLikes);
};
Note: I've seen people suggesting useEffect to this issue but I am not updating the state through useEffect here..
The problem I am having is that when a user selects id 7 for example, it triggers a function in App.tsx and filters the todo list data and update the state with the filtered list. But in the browser, it doesn't reflect the updated state immediately. It renders one step behind.
Here is a Demo
How do I fix this issue (without combining App.tsx and TodoSelect.tsx) ?
function App() {
const [todoData, setTodoData] = useState<Todo[]>([]);
const [filteredTodoList, setFilteredTodoList] = useState<Todo[]>([]);
const [selectedTodoUser, setSelectedTodoUser] = useState<string | null>(null);
const filterTodos = () => {
let filteredTodos = todoData.filter(
(todo) => todo.userId.toString() === selectedTodoUser
);
setFilteredTodoList(filteredTodos);
};
useEffect(() => {
const getTodoData = async () => {
console.log("useeffect");
try {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/todos"
);
setTodoData(response.data);
setFilteredTodoList(response.data);
} catch (error) {
console.log(error);
}
};
getTodoData();
}, []);
const handleSelect = (todoUser: string) => {
setSelectedTodoUser(todoUser);
filterTodos();
};
return (
<div className="main">
<TodoSelect onSelect={handleSelect} />
<h1>Todo List</h1>
<div>
{" "}
{filteredTodoList.map((todo) => (
<div>
<div>User: {todo.userId}</div>
<div>Title: {todo.title}</div>
</div>
))}
</div>
</div>
);
}
In TodoSelect.tsx
export default function TodoSelect({ onSelect }: TodoUsers) {
const users = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"];
return (
<div>
<span>User: </span>
<select
onChange={(e) => {
onSelect(e.target.value);
}}
>
{users.map((item) => (
<option value={item} key={item}>
{item}
</option>
))}
</select>
</div>
);
}
There's actually no need at all for the filteredTodoList since it is easily derived from the todoData state and the selectedTodoUser state. Derived state doesn't belong in state.
See Identify the Minimal but Complete Representation of UI State
Let’s go through each one and figure out which one is state. Ask three
questions about each piece of data:
Is it passed in from a parent via props? If so, it probably isn’t state.
Does it remain unchanged over time? If so, it probably isn’t state.
Can you compute it based on any other state or props in your component? If so, it isn’t state.
Filter the todoData inline when rendering state out to the UI. Don't forget to add a React key to the mapped todos. I'm assuming each todo object has an id property, but use any unique property in your data set.
Example:
function App() {
const [todoData, setTodoData] = useState<Todo[]>([]);
const [selectedTodoUser, setSelectedTodoUser] = useState<string | null>(null);
useEffect(() => {
const getTodoData = async () => {
console.log("useeffect");
try {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/todos"
);
setTodoData(response.data);
} catch (error) {
console.log(error);
}
};
getTodoData();
}, []);
const handleSelect = (todoUser: string) => {
setSelectedTodoUser(todoUser);
};
return (
<div className="main">
<TodoSelect onSelect={handleSelect} />
<h1>Todo List</h1>
<div>
{filteredTodoList
.filter((todo) => todo.userId.toString() === selectedTodoUser)
.map((todo) => (
<div key={todo.id}>
<div>User: {todo.userId}</div>
<div>Title: {todo.title}</div>
</div>
))
}
</div>
</div>
);
}
State update doesn't happen synchronously! So, selectedTodoUser inside filterTodos function is not what you're expecting it to be because the state hasn't updated yet.
Make the following changes:
Pass todoUser to filterTodos:
const handleSelect = (todoUser: string) => {
setSelectedTodoUser(todoUser);
filterTodos(todoUser);
};
And then inside filterTodos compare using the passed argument and not with the state.
const filterTodos = (todoUser) => {
let filteredTodos = todoData.filter(
(todo) => todo.userId.toString() === todoUser
);
setFilteredTodoList(filteredTodos);
};
You probably won't need the selectedTodoUser state anymore!
I have an input whose value is 4 or it can be any one and when I press the button it is generating another dynamic input, what I need is that each time a dynamic input is generated the value of each input is subtracted by -1 until it is in 1.
I have not been able to make it work as I need it, if someone can help me I would be very grateful, I have reviewed several examples but I have not been able to make it work, any help is welcome.
import { useState } from "react";
const defaultState = {
nombre: 4
};
function Row({ onChange, onRemove, nombre }) {
return (
<div>
<input
value={nombre}
onChange={e => onChange("nombre", e.target.value)}
placeholder="Decrementar"
/>
<button onClick={onRemove}>Eliminar</button>
</div>
);
}
export default function Pruebas() {
const [rows, setRows] = useState([defaultState]);
const handleOnChange = (index, name, value) => {
const copyRows = [...rows];
copyRows[index] = {
...copyRows[index],
[name]: value
};
setRows(copyRows);
};
const handleOnAdd = () => {
setRows(rows.concat(defaultState));
};
const handleOnRemove = index => {
const copyRows = [...rows];
copyRows.splice(index, 1);
setRows(copyRows);
};
return (
<div className="App">
{rows.map((row, index) => (
<Row
{...row}
onChange={(name, value) => handleOnChange(index, name, value)}
onRemove={() => handleOnRemove(index)}
key={index}
/>
))}
<button onClick={handleOnAdd}>-</button>
</div>
);
}
When you concatenate a new row make sure to decrement the field nombre's value see for a full example : https://codesandbox.io/s/stack-over-inc-3nixd
I'm new in ReactJS. I have a task - to do an app like Notes. User can add sublist to his notes, and note have to save to the state in subarray. I need to save sublist in the array inside object. I need to get state like this:
[...notes, { _id: noteId, text: noteText, notes: [{_id: subNoteId, text: subNoteText, notes[]}] }].
How can I to do this?
Sandbox here: https://codesandbox.io/s/relaxed-lamarr-u5hug?file=/src/App.js
Thank you for any help, and sorry for my English
const NoteForm = ({ saveNote, placeholder }) => {
const [value, setValue] = useState("");
const submitHandler = (event) => {
event.preventDefault();
saveNote(value);
setValue("");
};
return (
<form onSubmit={submitHandler}>
<input
type="text"
onChange={(event) => setValue(event.target.value)}
value={value}
placeholder={placeholder}
/>
<button type="submit">Add</button>
</form>
);
};
const NoteList = ({ notes, saveNote }) => {
const renderSubNotes = (noteArr) => {
const list = noteArr.map((note) => {
let subNote;
if (note.notes && note.notes.length > 0) {
subNote = renderSubNotes(note.notes);
}
return (
<div key={note._id}>
<li>{note.text}</li>
<NoteForm placeholder="Enter your sub note" saveNote={saveNote} />
{subNote}
</div>
);
});
return <ul>{list}</ul>;
};
return renderSubNotes(notes);
};
export default function App() {
const [notes, setNotes] = useState([]);
const saveHandler = (text) => {
const trimmedText = text.trim();
const noteId =
Math.floor(Math.random() * 1000) + trimmedText.replace(/\s/g, "");
if (trimmedText.length > 0) {
setNotes([...notes, { _id: noteId, text: trimmedText, notes: [] }]);
}
};
return (
<div>
<h1>Notes</h1>
<NoteList notes={notes} saveNote={saveHandler} />
<NoteForm
saveNote={saveHandler}
placeholder="Enter your note"
/>
</div>
);
}
The code in your saveHandler function is where you're saving your array of notes.
Specifically, this line:
setNotes([...notes, { _id: noteId, text: trimmedText, notes: [] }]);
But at the moment you're saving an empty array. What if you create another stateful variable called currentNote or something like that, relative to whatever note the user is currently working on within the application? While they are working on that note, the stateful currentNote object is updated with the relevant data, e.g. noteID, content, and parentID. Then, when the user has finished editing that particular note, by either pressing save or the plus button to add a new subnote, etc, that should fire a function such as your saveHandler to add the currentNote object to the "notes" array in the stateful "notes" variable. I'm not sure I like that the stateful variable notes contains an array within it called notes as well. I think this may cause confusion.
But in short, your setNotes line could change to something like (bear with me my JS syntax skills suck):
let newNotes= [...notes.notes];
newNotes.push(currentNote);
setNotes([...notes, { _id: noteId, text: trimmedText, notes: newNotes }]);
Wherein your stateful currentNote object is copied into the notes array on every save.