How to add sublist to Note application with ReactJS? - reactjs

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.

Related

useState in parent component not keep state, when parent component's callback function is called from child component

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.

React State and Events managed by Child Components

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

State hook variable not updated inside functional component

Upon updating the name of an ingredient, I want to submit this data is an ingredient with the up-to-date name: from "Milk" to "Cow's milk".
I've provided simple "1,2,3" steps as comments to briefly illustrate the flow of things, but you can assume that the console logged values you see here all happen right after I press the submit button (FloatingButtons):
export const Ingredient = ({
ingredient: ingredientProp,
}: IngredientProps) => {
const [name, setName] = useState(ingredientProp.name);
// Prints expected updated name: "Cow's milk"
console.log("name", name);
const [ingredient, setIngredient] = useState(ingredientProp);
// Prints expected updated ingredient containing the name: "Cow's milk"
console.log("ingredient", ingredient);
useEffect(() => {
// 2. Replace ingredient name with newName
// Prints expected updated name: "Cow's milk"
const newName = name;
console.log("newName", newName);
setIngredient({ ...ingredient, name: newName });
}, [name]);
return (
<form
className="Ingredient"
id={ingredientProp.id}
onSubmit={(e) => {
e.preventDefault();
console.log(ingredient);
// 3. Submit updated ingredient
// Prints ingredient with outdated name ("Milk"), why?
submitData(ingredient);
}}
>
<EditableField
defaultValue={name}
onChange={(newName) => {
console.log("newName", newName)
//1. Set name to newName
// Prints "Cow's milk", check!
setName(newName);
}}
/>
{/* Is a submit button that refers to the parent form */}
<FloatingButtons
formId={ingredientProp.id}
/>
</form>
);
};
I would think that you need to refactor your code a little bit
Create a handleSubmit function and wrap it around a useCallback hook
...
const handleSubmit = useCallback(() => {
submitData(ingredient);
}, [ingredient])
...
return <form
onSubmit={handleSubmit}
>
...
</form>
That's one way to do it, but you could also remove setIngredient since only name property will be changing; And that should give you the following
export const Ingredient = ({
ingredient: ingredientProp,
}: IngredientProps) => {
const [name, setName] = useState(ingredientProp.name);
const handleSubmit = useCallback(() => {
submitData({
...ingredient,
name,
});
}, [name])
return (
<form
className="Ingredient"
id={ingredientProp.id}
onSubmit={handleSubmit}
>
...
</form>
);
};
Thank you for your refactoring suggestion, that certainly helps but I did some more digging and realized the underlying issue was that there was more than one form with the same form id and that was somehow messing with the onSubmit event. I've now made sure that every form and its corresponding submit button have more specific ids.

How to Sort Form Input Automatically With React Hooks

I have a form where user can enter a name that will then be displayed on a list. Upon entering a new name the list should automatically be sorted in alphabetical order. Current attempt with useEffect does work but is buggy(list will only be sorted after user start deleting previous input text).
A few notable things to highlight with current setup:
Submission component is used for rendering list of names
Form component is used to store state of app and input fields
handleSortName() will execute sorting
useEffect() executes handleSortName() when there is a change to submissions value
import React, { useEffect, useState } from "react";
const Submission = ({ submission }) => {
return <div>name: {submission.name}</div>;
};
const Form = () => {
const [values, setValues] = useState({
name: ""
});
const [submissions, setSubmission] = useState([
{ name: "John" }
]);
const addSubmission = (values) => {
const newSubmissions = [...submissions, values];
setSubmission(newSubmissions);
};
const handleChange = (event) => {
const value = event.target.value;
setValues({ ...values, [event.target.name]: value });
};
const handleSubmit = (e) => {
e.preventDefault();
addSubmission(values);
handleSortName(submissions);
};
const handleSortName = (submissions) => {
return submissions.sort((a, b) => a.name.localeCompare(b.name));
};
useEffect(() => {
handleSortName(submissions);
}, [submissions]);
return (
<>
<form onSubmit={handleSubmit}>
<h1>Student Enrollment</h1>
<div>
<label>name: </label>
<input
required
type="text"
name="name"
value={values.name}
onChange={handleChange}
/>
<input type="submit" value="Submit" />
</div>
</form>
<h1>Submitted Student</h1>
{submissions.map((submission, index) => (
<Submission key={index} submission={submission} />
))}
</>
);
};
export default Form;
Working Sample: https://codesandbox.io/s/usestate-form-oj61v9?file=/src/Form.js
I am aware that useState is asynchronous and will not update value right away.
Any suggestion on other implementations such as functional updates, a custom hook or current UseEffect approach? Thanks in Advance!
UPDATE:
because React re-renders the component when the props or state changes. that means inside your handleSortName() function you have to call setSubmissions with the new sorted array, then React will know that the state was changed.
const handleSortName = (submissions) => {
// create a new copy of the array with the three dots operator:
let copyOfSubmissions = [...submissions];
// set the state to the new sorted array:
setSubmissions(
copyOfSubmissions.sort((a, b) => a.name.localeCompare(b.name))
);
};
or you can do both steps in 1 line:
const handleSortName = (submissions) => {
// set the state to the newly created sorted array with the three dots operator:
setSubmissions(
[...submissions].sort((a, b) => a.name.localeCompare(b.name))
);
};
sandbox link here

Adding a object to an array of object

My app is using react, redux and redux-thunk. I want to add a new product to https://fakestoreapi.com/products. The code below works, so I am happy with that, but it adds a single stringe. I want it to add a object of key-value pairs like that:
title: 'test product',
price: 13.5,
description: 'lorem ipsum set',
image: 'https://i.pravatar.cc',
category: 'electronic'
My code:
import React from "react";
export const ProductForm = ({ addProduct }) => {
const [product, setProduct] = React.useState("");
const updateProduct = (event) => {
setProduct(event.target.value);
};
const onAddProductClick = () => {
addProduct(product);
setProduct("");
};
return (
<div>
<input
onChange={updateProduct}
value={product}
type="text"
name="title"
placeholder="title"
/>
<button onClick={onAddProductClick}>Add product</button>
</div>
);
};
I know that i have to use a form to do this task. Although I don't know how to change the code below so that it woudl still works. If in form onSubmit i use same function as onClik in code above, the page refresh itself and do not add a product to an array.
You are setting product to the value from the input here:
const updateProduct = (event) => {
setProduct(event.target.value);
};
event.target.value is a string. It's whatever you enter in your input.
If you want an object you can:
change product to productName and use productName in onAddProductClick. Like addProduct({title: productName})
OR
change updateProduct to:
const updateProduct = (event) => {
setProduct({title: event.target.value});
};

Resources