React Forms 📝

✅ Objectives

  • 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

What is a controlled input

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.

controlled input diagram

Making an input controlled

To keep track of each input’s value, you need:

  1. State for the input that will manage the input’s value

  2. A value attribute on the input that corresponds to that piece of state

  3. An onChange listener attached to the input to monitor users behavior and update state as the user interacts with the field

Form components also need an onSubmit listener on the form element to handle the submitted form data.

Examples

🤗 Reconciliation 🤗

  • When setState is called, React will re-render that component and all of its children
  • This is an expensive operation, so React optimizes by running a diffing algorithm to decide which components actually need to trigger committed changes to the DOM.
  • This diffing process is called reconciliation
  • During reconciliation, React compares its own picture of the current state of the DOM tree with what it should look like after the change. Using this diff, the minimal DOM manipulation necessary is committed to reconcile the current DOM tree with what it should be after the change to state.

Why we don’t mutate state directly

  • One of the choices made in the reconciliation process is to only commit to updating a component in the DOM if one of its nodes or property values has changed.
  • If all nodes (types of React elements) and their props and values are the same, React will leave that component unchanged from the previous render.
  • If an object or array is mutated directly and then set as the new value for state React sees the same object in state as the previous render and leaves the DOM untouched
// so don't do this because it won't update the DOM
state.prop = "New Value"
setState(state);

// do this instead because it will update the DOM
// because React will see the state is set to a new object
setState({...state, prop: "New Value"})

🛠️ ProjectForm setup

1. For each input element in the form, create a new state variable:
const [name, setName] = useState("");
const [about, setAbout] = useState("");
const [phase, setPhase] = useState("");
const [link, setLink] = useState("");
const [image, setImage] = useState("");

A more elegant approach is to create a state object with key/value pairs associated with each form field:

const [formData, setFormData] = useState({
  name: "",
  about: "",
  phase: "",
  link: "",
  image: "",
});

Note: This approach works well for a form that has multiple string, number, textarea, & select inputs but gets a bit clunkier when the form includes inputs like checkboxes or files. React docs recommend an external library like Formik as a complete solution for forms.

React Hook Form is another approach to handling forms in React that has become popular as well.

2. Connect the value attribute of each input field to the corresponding state variable:

Example:

<input
  type="text"
  id="name"
  value={formData.name}
/>

Note: The reason formData.name is being used is because the state variable is an object named formData. To access the value of a key within the object, dot notation is used.

3. Add an onChange listener for each input field using a helper function: Example:

<input 
  type="text" 
  id="name" 
  value={formData.name} 
  onChange={handleOnChange} 
/>

🤯 If using individual pieces of state for form fields, a separate helper function will be created for each corresponding field.

Example:

<input type="text" id="about" onChange={handleAboutChange} />
<input type="text" id="phase" onChange={handlePhaseChange} />

4. Adding a name attribute to the input fields:

<input
  type="text"
  id="link"
  onChange={handleOnChange}
  value={formData.link}
  name="link"
/>

IMPORTANT: The name attribute needs to match with the key created in the state object in order to update the value. If the key in the state object is ‘link’ then the name attribute for the corresponding input field should be link as well

5. Updating the state when the onChange occurs (aka when the user begins typing or changing parts of the form):

const handleOnChange = (e) => {
  // e.target will return an object, the element that triggered the event with properties
  // including name and value. Object destructuring is used to extract that values from e.target

  const { name, value } = e.target;

  // This is the same as doing:
  // const name = e.target.name
  // const value = e.target.value

  // The setter function is then invoked and a new object will  be created with the 
  // contents of the previous formData spread and the new key/value added to avoid overwriting the 
  // previous form field values

  setFormData((formData) => ({ ...formData, [name]: value }));
};

6. On the <form> element, add an onSubmit listener with a handleSubmit helper function that will run when the form is submitted:

<form className="form" autoComplete="off" onSubmit={handleSubmit}>
</form>
const handleSubmit = (e) => {
  e.preventDefault();
};

🔑 After the form has been submitted

The state of projects is defined inside of the parent component App and the behavior occurs in the child component ProjectForm. When the new project is submitted, projects will need to be updated to include it.

💡 What do we need to do?

Implement the Inverse Data Flow Pattern!

Inverse Data Flow for adding projects

Here is where the process of inverse data flow will need to occur:
  1. Create a helper function in App component called onAddProject that will update the projects state:
const onAddProject = (newProject) => {
  setProjects(projects => [...projects, newProject]);
};
And in the JSX:
<ProjectForm onAddProject={onAddProject} />

Inside the ProjectForm component, destructure onAddProject from the props and invoke it from within the handleSubmit function, passing it the formData object:

const handleSubmit = (e) => {
  e.preventDefault();
  onAddProject(formData);

  // after we have delivered the formData to the App component 
  // and updated state, clear the form by setting the values
  // back to empty strings:
  setFormData({
    name: "",
    about: "",
    phase: "",
    link: "",
    image: "",
  });
};

💡 Conclusion

  • State is a very integral part of the way that React applications render and manipulate the DOM.
  • React prefers using state to update the forms and keep track of the form fields values, making them controlled inputs, rather than letting form inputs manage their own internal state (through their value).
  • What our user sees in the input fields reflects the value of the state associated with that field.
  • Example: Doing this allows us to make an edit form populated with a project’s previously saved values for the inputs by setting the formState to match the saved record.