- Explain the difference between a controlled and uncontrolled input
- Explain why controlled inputs are preferred by the React community
- Review how to use callback functions with events in React
- Review how to change parent state from a child component
-
Initialize state for all the form fields found in the component
-
Add an
onChange
event to each field that will update state associated to the field that is interacted with -
Provide a
value
attribute to each form field that will return the associated piece of state -
Add an
onSubmit
event handler to the form
-
When the form is submitted:
-
Update the
projects
state located in the parent component,App
using inverse data flow-
Use the spread operator to return a new array with the new project included
-
Set the
projects
state to the new array value
-
-
- For each input element in the form, create a new state variable
- Connect the
value
attribute of each input field to the corresponding state variable - Create an
onChange
handler for each input field to update the corresponding state variable - On the
<form>
element, create anonSubmit
listener and attach ahandleSubmit
handler to run code when the form is submitted
In vanilla JS, our typical process for working with forms and getting access to the form data in our application looked something like this:
- Get the form element and listen for a submit event
- Find the form inputs using their name attribute and grab the values
- Do something with the form data (send a
fetch
request; update the DOM)
const form = document.querySelector("form");
form.addEventListener("submit", (event) => {
event.preventDefault();
// access form data from the DOM
const nameInput = event.target.name;
const passwordInput = event.target.password;
const formData = {
name: nameInput.value,
password: passwordInput.value,
};
// do something with the form data
});
In React, rather than looking into the DOM to get the form's input field values when the form is submitted, we use state to monitor the user's input as they type, so that our component state is always in sync with the DOM.
To keep track of each input's value, you need:
- Some state to manage the input
- An
onChange
listener on the input to monitor user input and update state - A
value
attribute on the input that corresponds to a key in state
And for the form itself, you need an onSubmit
listener on the form to finally
submit data.
For example, if we have a form component that looks like this:
function CommentForm() {
const [username, setUsername] = useState("");
const [comment, setComment] = useState("");
return (
<form>
<input type="text" name="username" />
<textarea name="comment" />
<button type="submit">Submit</button>
</form>
);
}
We could make it a controlled form by attaching onChange listeners to each input:
function CommentForm() {
const [username, setUsername] = useState("");
const [comment, setComment] = useState("");
function handleUsernameChange(event) {
setUsername(event.target.value);
}
function handleCommentChange(event) {
setComment(event.target.value);
}
return (
<form>
<input type="text" name="username" onChange={handleUsernameChange} />
<textarea name="comment" onChange={handleCommentChange} />
<button type="submit">Submit</button>
</form>
);
}
Doing this creates a 1-way connection wherein user input changes state
. This
is called an uncontrolled form.
To make it a 2-way street wherein state
can change the user's input, we add a
value
attribute to our inputs.
<form>
<input
type="text"
name="username"
onChange={handleUsernameChange}
value={username}
/>
<textarea name="comment" onChange={handleCommentChange} value={comment} />
</form>
When the form actually submits, it's often helpful to pass the state from the form up to a parent component. Imagine we have an app like this:
CommentContainer
/ \
CommentForm CommentCard
When the user submits out the comment form, a new CommentCard
should be rendered. The CommentContainer
holds an array of comments in state, so it needs to be updated when a new comment is added. To achieve this, we need to pass down a callback function from the CommentContainer
to the CommentForm
as a prop:
function CommentContainer() {
const [comments, setComments] = useState([])
const commentCards = comments.map((comment, index) => (
<CommentCard key={index} comment={comment} />
))
// callback for adding a comment to state
function addComment(newComment) {
setComments([...comments, comment]);
};
render() {
return (
<section>
{commentCards}
<hr />
<CommentForm onAddComment={addComment} />
</section>
);
}
}
When the user submits the comment, we can use the handleCommentSubmit
callback in the onSubmit
event in the CommentForm
:
function CommentForm({ onAddComment }) {
const [username, setUsername] = useState("");
const [comment, setComment] = useState("");
function handleUsernameChange(event) {
setUsername(event.target.value);
}
function handleCommentChange(event) {
setComment(event.target.value);
}
function handleSubmit(event) {
event.preventDefault();
const newComment = {
username,
comment,
};
onAddComment(newComment);
}
return (
<form onSubmit={handleSubmit}>
<input type="text" name="username" onChange={handleUsernameChange} />
<textarea name="comment" onChange={handleCommentChange} />
<button type="submit">Submit</button>
</form>
);
}
These are some common strategies for updating arrays in state without mutating the original array.
- adding an item: use spread operator -
setItems([...items, newItem])
- removing an item: use filter -
setItems(items.filter(i => i.id !== id))
- updating an item: use map -
setItems(items.map(i => i.id === newItem.id ? newItem : item))
- Use the spread operator!
function addComment(newComment) {
// spread to create a new array and add new comment at the end
const updatedComments = [...comments, newComment];
setComments(updatedComments);
}
- Use filter!
function removeComment(commentId) {
// filter to return a new array with the comment we don't want removed
const updatedComments = comments.filter(
(comment) => comment.id !== commentId
);
setComments(updatedComments);
}
- Use map!
function updateComment(updatedComment) {
// filter to return a new array with the comment we don't want removed
const updatedComments = comments.map((comment) => {
if (comment.id === updatedComment.id) {
// if the comment in state is the one we want to update, replace it with the new updated object
return updatedComment;
} else {
// otherwise return the original object
return comment;
}
});
setComments(updatedComments);
}
If you only want to update one attribute instead of replacing the whole object:
// updating one object in an array
function updateCustomer(id, name) {
// use map to return a new array so we aren't mutating state
const updatedCustomers = customers.map((customer) => {
// in the array, look for the object we want to update
if (customer.id === id) {
// if we find the object
// make a copy of it and update whatever attribute have changed
return {
...customer,
name: name,
};
} else {
// for all other objects in the array
return customer; // return the original object
}
});
// set state with our updated array
setCustomers(updatedCustomers);
}