Skip to content

Latest commit

 

History

History

04_react_forms

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

React Forms

SWBATs:

  • 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

Deliverables

1. Make the ProjectForm component a controlled 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

2. Handle submitting the form and update state in parent using inverse data flow

  • 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

Using Forms

Process: Making a Controlled Form

  1. For each input element in the form, create a new state variable
  2. Connect the value attribute of each input field to the corresponding state variable
  3. Create an onChange handler for each input field to update the corresponding state variable
  4. On the <form> element, create an onSubmit listener and attach a handleSubmit handler to run code when the form is submitted

HTML Forms

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

React Controlled Forms

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:

  1. Some state to manage the input
  2. An onChange listener on the input to monitor user input and update state
  3. 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>

Inverse Data Flow

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

Advanced State Updates: Arrays

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

Adding to an array

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

Removing from an array

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

Updating an item in an array

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