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.
Related
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
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});
};
Isn't the hook useCallback supposed to return an updated function every time a dependency change?
I wrote this code sandbox trying to reduce the problem I'm facing in my real app to the minimum reproducible example.
import { useCallback, useState } from "react";
const fields = [
{
name: "first_name",
onSubmitTransformer: (x) => "",
defaultValue: ""
},
{
name: "last_name",
onSubmitTransformer: (x) => x.replace("0", ""),
defaultValue: ""
}
];
export default function App() {
const [instance, setInstance] = useState(
fields.reduce(
(acc, { name, defaultValue }) => ({ ...acc, [name]: defaultValue }),
{}
)
);
const onChange = (name, e) =>
setInstance((instance) => ({ ...instance, [name]: e.target.value }));
const validate = useCallback(() => {
Object.entries(instance).forEach(([k, v]) => {
if (v === "") {
console.log("error while validating", k, "value cannot be empty");
}
});
}, [instance]);
const onSubmit = useCallback(
(e) => {
e.preventDefault();
e.stopPropagation();
setInstance((instance) =>
fields.reduce(
(acc, { name, onSubmitTransformer }) => ({
...acc,
[name]: onSubmitTransformer(acc[name])
}),
instance
)
);
validate();
},
[validate]
);
return (
<div className="App">
<form onSubmit={onSubmit}>
{fields.map(({ name }) => (
<input
key={`field_${name}`}
placeholder={name}
value={instance[name]}
onChange={(e) => onChange(name, e)}
/>
))}
<button type="submit">Create object</button>
</form>
</div>
);
}
This is my code. Basically it renders a form based on fields. Fields is a list of objects containing characteristics of the field. Among characteristic there one called onSubmitTransformer that is applied when user submit the form. When user submit the form after tranforming values, a validation is performed. I wrapped validate inside a useCallback hook because it uses instance value that is changed right before by transform function.
To test the code sandbox example please type something is first_name input field and submit.
Expected behaviour would be to see in the console the error log statement for first_name as transformer is going to change it to ''.
Problem is validate seems to not update properly.
This seems like an issue with understanding how React lifecycle works. Calling setInstance will not update instance immediately, instead instance will be updated on the next render. Similarly, validate will not update until the next render. So within your onSubmit function, you trigger a rerender by calling setInstance, but then run validate using the value of instance at the beginning of this render (before the onSubmitTransformer functions have run).
A simple way to fix this is to refactor validate so that it accepts a value for instance instead of using the one from state directly. Then transform the values on instance outside of setInstance.
Here's an example:
function App() {
// setup
const validate = useCallback((instance) => {
// validate as usual
}, []);
const onSubmit = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
const transformedInstance = fields.reduce((acc, {name, onSubmitTransformer}) => ({
...acc,
[name]: onSubmitTransformer(acc[name]),
}), instance);
setInstance(transformedInstance);
validate(transformedInstance);
}, [instance, validate]);
// rest of component
}
Now the only worry might be using a stale version of instance (which could happen if instance is updated and onSubmit is called in the same render). If you're concerned about this, you could add a ref value for instance and use that for submission and validation. This way would be a bit closer to your current code.
Here's an alternate example using that approach:
function App() {
const [instance, setInstance] = useState(/* ... */);
const instanceRef = useRef(instance);
useEffect(() => {
instanceRef.current = instance;
}, [instance]);
const validate = useCallback(() => {
Object.entries(instanceRef.current).forEach(([k, v]) => {
if (v === "") {
console.log("error while validating", k, "value cannot be empty");
}
});
}, []);
const onSubmit = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
const transformedInstance = fields.reduce((acc, {name, onSubmitTransformer}) => ({
...acc,
[name]: onSubmitTransformer(acc[name]),
}), instanceRef.current);
setInstance(transformedInstance);
validate(transformedInstance);
}, [validate]);
// rest of component
}
I have a code like this
const ChildComponent = ({ products, setProducts }) => (
<form>
<input type="text" value={products.name} onChange={(e) => setProducts(e.target.value)} />
<input type="submit" value="Finish" />
</form>
)
const ParentComponent = () => {
const [products, setProducts] = useState(
{
id: 1,
name: "Test",
}
);
useEffect(() => {
// Where i call API to get list product and set it to child component
}, [])
return <ChildComponent products={products} setProducts={setProducts} />
}
for some reason , i can ONLY update state of ParentComponent in ChildComponent. It's work but i think it's so weird, that look like i change props of child component everytime when i make a edit of input. Can any one tell me that is an anti pattern or not.Sorry about my bad English. Thank you so much!
It's not an anti-pattern to pass the state object and state updater function as props, but this offloads the responsibility to update state correctly and maintain the state invariant to consuming components.
As you can see, your child component already messes up and changes the state shape/invariant from Object to String.
const [products, setProducts] = useState({ // <-- object
id: 1,
name: "Test",
});
... child
onChange={(e) => setProducts(e.target.value)} // <-- string value
On the subsequent render attempting to access value={products.name} in the child will fail as now products is a string.
I typically suggest declaring a handler function to do the state update and pass that instead.
In your snippet it seems the child component is more a "controlled input" meaning it's an input tag with a value and onChange handler. This is an example refactor I would do.
const ChildComponent = ({ value, onChange, onSubmit }) => (
<form onSubmit={onSubmit}>
<input type="text" value={value} onChange={onChange} />
<input type="submit" value="Finish" />
</form>
)
const ParentComponent = () => {
const [products, setProducts] = useState({
id: 1,
name: "Test",
});
const changeHandler = e => {
setProducts(products => ({
...products,
name: e.target.value,
}));
};
const onSubmit = e => {
e.preventDefault();
// handle the form submission
};
useEffect(() => {
// Where i call API to get list product and set it to child component
}, []);
return (
<ChildComponent
value={products}
onChange={changeHandler}
onSubmit={submitHandler}
/>
);
}
This way the parent maintains control over both the state updates and how the form data is submitted. The child hasn't any idea what the value represents and it isn't trying to update anything in any way, but simply passing back out the events.
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.