cf.completefrontend
Open lab My progress
THE REACT FIELD GUIDE

Reacting to input with state

Build a form the React way: list its visual states, find what triggers each change, and swap clashing booleans for one status. Live examples to try.

By the end, you can
  • 01
    Think declarativelySay why a form that describes each look can't get stuck the way hand-written show and hide commands can.
  • 02
    List looks and triggersDraw a component's visual states with a mock or a living styleguide, and list the human and computer inputs that move between them.
  • 03
    Remove impossible statesReplace booleans that can clash with one status, and work out anything another piece of state already says.

A coupon box that locks you out

A checkout page often has a box for a coupon code. You type a code, press Apply, and the shop asks its server whether the code is real. While it waits, the form should say so. Then it either shows the discount or explains what went wrong. Here is that box written with plain JavaScript, changing the page by hand. The result is live. Type TEA5 in the box, press Apply, and wait one second:

A coupon box by hand (with a bug on purpose)Pop out in the code editor (opens in a new tab)JavaScript
const form = document.querySelector("#coupon-form");const input = document.querySelector("#code");const button = document.querySelector("#apply");const checking = document.querySelector("#checking");const errorText = document.querySelector("#error");const success = document.querySelector("#success"); async function handleSubmit(e) {  e.preventDefault();  input.disabled = true;  button.disabled = true;  checking.hidden = false;  errorText.hidden = true;  try {    await checkCoupon(input.value);    form.hidden = true;    success.textContent = "Coupon " + input.value + " applied: 10% off.";    success.hidden = false;  } catch (err) {    checking.hidden = true;    errorText.textContent = err.message;    errorText.hidden = false;    button.disabled = false;  }} function handleInput() {  button.disabled = input.value.length === 0;} form.addEventListener("submit", handleSubmit);input.addEventListener("input", handleInput);
ResultLoading…

Lines 1 to 6 find the six parts of the page. handleSubmit (lines 8 to 25) runs when you press Apply. Line 9 stops the browser from sending the form and loading a new page. Lines 10 to 13 turn the box and the button off and show "Checking your code…". Line 15 waits for the pretend server in the api.js tab: it answers after one second, accepts TEA10, and refuses anything else. If it accepts, lines 16 to 18 hide the form and show the discount. If it refuses, lines 20 to 23 hide the waiting message, show the error, and turn Apply back on.

Now try to fix your code. You can't: the box is still grey. Nothing on lines 20 to 23 turns the box back on, so the page asks you to check your code and won't let you change it. The bug is on purpose here, and it's the kind that hand-written page updates are full of: each event lists its own changes, and one list missed one.

You met this problem with a shopping cart in Why React?. A form makes it worse, because a form has many looks: empty, being typed in, waiting for the server, done, or showing an error. Here is the same box in React. Try TEA5 again, then change it to TEA10 and press Apply:

The same coupon box with ReactPop out in the code editor (opens in a new tab)JSX
import { useState } from "react";import { checkCoupon } from "./api.js"; export default function App() {  const [code, setCode] = useState("");  const [error, setError] = useState(null);  const [status, setStatus] = useState("typing");   if (status === "success") {    return <p>Coupon {code} applied: 10% off.</p>;  }   async function handleSubmit(e) {    e.preventDefault();    setStatus("submitting");    setError(null);    try {      await checkCoupon(code);      setStatus("success");    } catch (err) {      setStatus("typing");      setError(err);    }  }   return (    <form onSubmit={handleSubmit}>      <label>        Coupon code{" "}        <input          value={code}          onChange={(e) => setCode(e.target.value)}          disabled={status === "submitting"}        />      </label>      <button disabled={code.length === 0 || status === "submitting"}>Apply</button>      {status === "submitting" && <p>Checking your code…</p>}      {error !== null && <p>{error.message}</p>}    </form>  );}
ResultLoading…

This time you can fix the code after the error. Lines 5 to 7 keep three pieces of state: what's in the box, the last error, and a status that names what the form is doing. The JSX below them doesn't give the page orders. Line 33 describes the box: it's off while status is "submitting", and on the rest of the time. There is no line that turns it back on, because there is nothing to forget. Every render works the box out again from the state.

That's the idea of this lesson. In React you don't change parts of the page in response to events. You describe each look the component can have, and when something happens, you set state to pick a look. The rest of the lesson builds this form step by step, the way you would build any form in React.

Five columns, one per look of the coupon form. Empty: box empty, Apply off. Typing: box TEA5, Apply on. Submitting: box off, Apply off, Checking. Success: Applied, 10%. Error: box TEA5, Apply on, Not valid.Emptybox: emptyApply: offTypingbox: TEA5Apply: onSubmittingbox: offApply: offChecking…SuccessApplied: 10%Errorbox: TEA5Apply: onNot validone component, five descriptions
The coupon form has five looks. In React you describe each of them once, and the component's state picks the one the page shows.
Real-life analogyScenes on a stage lighting desk

A theatre doesn't move twenty sliders by hand at every moment of a play. The lighting team saves each scene on the desk ahead of time. During the show, the operator follows the cue sheet and presses one button, and the desk sets every lamp for that scene. No lamp can be left in the wrong place from an earlier scene.

In real life: Each scene saved on the desk: which lamps are on, and how bright
In React: Each look described in JSX for one status
In real life: Pressing the button for scene 3
In React: Calling setStatus("submitting")
In real life: The cue sheet: when to change scenes
In React: The list of triggers: Apply, the server's answer
In real life: The desk shows one scene at a time
In React: The status holds one value at a time
The main idea

To react to input with state, you describe every look a component can have, and let state choose between them. Find the looks, find what moves the component from one look to another, keep the smallest state that names the look, and set that state in your event handlers. React then updates the page to match.

Commands for each event, or a description of each look

The by-hand box is imperative: each handler gives the page a list of commands for one event. "Turn the box off. Turn Apply off. Show the waiting message." The React box is declarative: each part of the page says what it looks like for any state. To see the difference, follow each part of the form through both versions. Each row is one part of the form; the middle column lists the by-hand lines that change it, and the last column the React line that describes it:

Who keeps each part of the coupon form right
Part of the formBy hand, changed onWith React, described on
The coupon box, on or offLine 10 turns it off. No line turns it back on: the bug.Line 33: disabled={status === "submitting"}.
Apply, on or offLines 11, 23, and 28, in two different handlers.Line 36, for every look.
The waiting messageLine 12 shows it, line 20 hides it.Line 37: shown while submitting.
The error messageLines 13, 21, and 22.Line 38: shown while there is an error.
The form, or the discountLines 16 to 18 hide the form and show the discount.Lines 9 to 11: an early return for success.

By hand, the box is changed in one place, and needed two. Apply is changed in three places, in two different handlers. Every new event, like a "Remove coupon" button, would need its own list of commands, and each list has to know the state that every other list left behind. With React, each part has one line, and that line covers every look at once. Adding a new event means setting state; the descriptions already handle it.

Both versions end up making the same kind of DOM change. React turns the box off and on for you (it adds or removes the disabled attribute), after it compares the new description with the last one. Render and commit follows those steps.

Step 1: list every look the form can have

Before you write any logic, list the looks a user can see. Designers call them visual states. For the coupon form there are five:

  • Empty: the box is empty, so Apply is off.
  • Typing: there is a code in the box, so Apply is on.
  • Submitting: the box and Apply are off, and the form says it's checking.
  • Success: the form is gone, and a message says the coupon is applied.
  • Error: like Typing, with the server's error message under the form.

Then draw each look, with no logic yet. In React, a quick way to draw them is a mock: a component that takes the look as a prop and shows it. Here status is a prop with a default value, "empty":

A mock of the coupon form, picked by a status propPop out in the code editor (opens in a new tab)JSX
export default function CouponForm({ status = "empty" }) {  if (status === "success") {    return <p>Coupon TEA10 applied: 10% off.</p>;  }  return (    <form onSubmit={(e) => e.preventDefault()}>      <label>        Coupon code <input disabled={status === "submitting"} />      </label>      <button disabled={status === "empty" || status === "submitting"}>Apply</button>      {status === "submitting" && <p>Checking your code…</p>}      {status === "error" && <p>That code isn't valid. Check it and try again.</p>}    </form>  );}
ResultLoading…

Line 1 takes status and uses "empty" when no one passes it. Lines 2 to 4 return the success message instead of the form (an early return, from Conditional rendering). Line 6 stops the browser from sending the form, so pressing Apply in a mock does nothing. Line 8 turns the box off only while submitting. Line 10 turns Apply off when the box is empty or while submitting. Lines 11 and 12 show a message for two of the looks.

Press Edit & run and change "empty" on line 1 to "success". The result shows Coupon TEA10 applied: 10% off.. Try "error" and "submitting" too. You are checking the design before any of it works.

Or flip between the looks with the buttons below. The form is the same mock, rendered by React on this page; the highlighted lines are the ones whose condition is true for the look you pick, and the table reads what ended up on the page.

Playground: flip the coupon form between its looks
The mock, with the lines this look turns onJSX
export default function CouponForm({ status = "empty" }) {  if (status === "success") {    return <p>Coupon TEA10 applied: 10% off.</p>;  }  return (    <form onSubmit={(e) => e.preventDefault()}>      <label>        Coupon code <input disabled={status === "submitting"} />      </label>      <button disabled={status === "empty" || status === "submitting"}>Apply</button>      {status === "submitting" && <p>Checking your code…</p>}      {status === "error" && <p>That code isn't valid. Check it and try again.</p>}    </form>  );}
The page<CouponForm status="empty" />
Try it yourself
status = "empty"

The highlighted line is where a condition is true for "empty". Everything else on the page comes from the same lines with their conditions false.

The page pane shows the mock above, rendered by React on this page with the status you pick. The table reads the real input, button, and messages after each render. Reset goes back to Empty.

Pick Submitting: lines 8, 10, and 11 light up, so the box and Apply are off and the waiting message shows. Pick Typing: no line lights up. Every condition is false, which is exactly the look you want: both on, no message.

When a component has many looks, it helps to see them all at once. Render the mock once per look on one page. A page like this is called a living styleguide, because it shows the real component, not a picture of it:

A living styleguide: every look on one pagePop out in the code editor (opens in a new tab)JSX
import CouponForm from "./CouponForm.jsx"; const statuses = ["empty", "typing", "submitting", "success", "error"]; export default function App() {  return (    <>      {statuses.map((status) => (        <section key={status}>          <h4>Coupon form: {status}</h4>          <CouponForm status={status} />        </section>      ))}    </>  );}
ResultLoading…

The mock is now in the CouponForm.jsx tab, and App.jsx imports it on line 1. Line 3 lists the five looks. Lines 8 to 13 use map to draw a heading and a CouponForm for each one, with status as the key. Change line 10 of CouponForm.jsx, say to turn Apply off in the error look too, and all five forms update at once. Tools like Storybook are built on this idea.

Step 2: find what triggers each change

Next, find what moves the form from one look to another. Changes come from two kinds of input:

  • Human inputs: the user types, clicks, or presses a key. You handle them with event handlers, from Responding to events.
  • Computer inputs: the server answers, a timer ends, an image finishes loading. You handle them in the code that waits for them, like the code after await.

Either way, the screen changes only because the code sets state. For the coupon form, the changes are:

What moves the coupon form from one look to another
FromToTriggerKind of input
EmptyTypingYou type a letter.Human
TypingEmptyYou delete the last letter.Human
TypingSubmittingYou press Apply.Human
SubmittingSuccessThe server accepts the code.Computer
SubmittingErrorThe server refuses the code.Computer
ErrorSubmittingYou press Apply again.Human

Read each row as "in this look, this happens, so the form moves to that look". A good way to check the list is to draw it: a card for each look and an arrow for each change.

Five look cards joined by six arrows. Empty to Typing: you type. Typing to Empty: you delete all. Typing to Submitting: you press Apply. Submitting to Success: the server says yes. Submitting to Error: the server says no. Error to Submitting: you press Apply again.EmptyTypingSubmittingSuccessErroryou: typeyou: delete allyou: Applyserver: yesserver: noyou: Apply again
Each arrow is one change, labelled with its trigger. Four come from you, a human input. Two come from the server, a computer input. Every arrow ends in a state update.

The drawing also shows what can't happen. No arrow leaves Success: once the coupon is applied, the form is gone. No arrow goes from Empty to Submitting: Apply is off while the box is empty. And typing doesn't move the form out of Error: the message stays until you press Apply again. If you find an arrow that you didn't plan, you found a bug before writing any code.

Now watch the arrows happen. The form below is the overview's React coupon box, running on this page, and the log lists each change of look with the input that caused it:

Playground: what moved the form to its new look?
The coupon formtry TEA5, then TEA10

Changes of look

  1. No changes yet.
Try it yourself

Look now: Empty

Type TEA5 in the box and press Apply. Then fix it to TEA10 and press Apply again. Each change of look is logged with what triggered it.

The coupon form runs here with React, the same state and handler as the overview's, and the same pretend server: one second, then TEA10 is accepted and anything else is refused. Only changes of look are logged; typing that keeps the look adds no line. Reset starts a new form.

Type TEA5: the first letter logs Empty to Typing, and the other letters log nothing, because the look stays Typing. Press Apply: a human input moves it to Submitting. One second later, a computer input, the server's answer, moves it to Error. Change the code to TEA10 and press Apply again: Error to Submitting, then Submitting to Success. Each line is one arrow in the drawing.

Steps 3 and 4: keep only the state you need

Now hold the looks in state with useState. Each piece of state is something that can change, and something that can go wrong, so you want as few as possible. A first try often has one true-or-false value, a boolean, per look, plus the code and the error. It covers every look, so it works:

A first try: a boolean for every lookJavaScript
const [code, setCode] = useState("");const [error, setError] = useState(null);const [isEmpty, setIsEmpty] = useState(true);const [isTyping, setIsTyping] = useState(false);const [isSubmitting, setIsSubmitting] = useState(false);const [isSuccess, setIsSuccess] = useState(false);const [isError, setIsError] = useState(false);

Lines 1 and 2 keep the code and the last error. Lines 3 to 7 keep one boolean per look: isEmpty starts true, and the others start false. That's seven pieces of state, and every handler must set the right ones. Pressing Apply would need setIsTyping(false) and setIsSubmitting(true); an error would need setIsSubmitting(false), setIsError(true), and setError(err). Forget one, and the state says two things at once.

Five booleans can be set in 2 × 2 × 2 × 2 × 2 = 32 ways. Only 5 of them, where exactly one is true, are a look the form really has. The other 27 are impossible states: combinations the state allows, but that no user should ever see. Switch the booleans below and watch the form the first try would draw:

Playground: switch the five booleans
The first try draws the form from its booleansJSX
if (isSuccess) {  return <p>Coupon TEA10 applied: 10% off.</p>;}return (  <form>    <label>      Coupon code <input disabled={isSubmitting} />    </label>    <button disabled={isEmpty || isSubmitting}>Apply</button>    {isSubmitting && <p>Checking your code…</p>}    {isError && <p>That code isn't valid. Check it and try again.</p>}  </form>);
The pagepossible
Try it yourself
A real look

Possible. Only isEmpty is true, so this is the empty look.

React draws the form on this page from the five booleans, with the first try's code shown here. You have tried 1 of the 32 combinations, and 0 of them were impossible. Only 5 of the 32 are real looks. Reset goes back to the starting state, where only isEmpty is true.

Turn on isError while isSubmitting is on: the page shows the error and turns the box off, the same trap as the by-hand bug. Turn on isEmpty and isTyping together: Apply stays off while the box has a code. Notice too that isTyping changes nothing on the page: the drawing never reads it.

Left: 32 small squares, the ways five booleans can be set. Five are solid and marked ok, the real looks; the other 27 are dashed and crossed out, impossible. Right: one status with three cards, typing, submitting, and success, all real.five booleans: 32 waysokokokokok5 real looks, 27 impossibleone status: 3 values"typing""submitting""success"every value is real
Five true-or-false state variables can be set 32 ways, and only 5 of them are a real look. A status holds one value at a time, so each of its values is a state the form can really be in.

To remove the impossible states, ask three questions about each piece of state. Each row is one question, what it finds in the first try, and what to do about it:

Three questions that shrink the state
QuestionIn the first tryWhat to do
Can two of them be true when they shouldn't be?isTyping, isSubmitting, and isSuccess can all be true at once.Replace the three with one status.
Does another piece of state already say it?isEmpty is true exactly when code.length === 0.Remove it, and check code.length during render.
Is it the opposite of another piece of state?isError is true exactly when error isn't null.Remove it, and check error !== null.

After the clean-up, three pieces of state are left, down from seven:

The state that's left: three piecesJavaScript
const [code, setCode] = useState("");const [error, setError] = useState(null);const [status, setStatus] = useState("typing"); // "typing", "submitting", or "success"

Line 1 keeps the code and line 2 the last error, as before. Line 3 replaces three booleans with one status that holds one of three strings. It can't be "typing" and "submitting" at once, because a variable holds one value. A set of names like this, one of which is chosen, is often called an enum. JavaScript has no enum type, so a string does the job; the comment lists the three allowed values.

The five looks are all still there. Each one is worked out from the three pieces:

  • Empty: status is "typing" and code.length === 0.
  • Typing: status is "typing", the box has a code, and error is null.
  • Submitting and Success: status says so.
  • Error: status is "typing" and error !== null.

Could error go into status as a fourth value, "error"? Yes, and that works well too. The message still needs a place, though, and it only makes sense in the error look. TypeScript can say exactly that with a discriminated union: a type with one shape per status, where only the error shape has a message. This one has an error on purpose:

A status union where only errors have a message (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type CouponState =  | { status: "typing" }  | { status: "submitting" }  | { status: "success" }  | { status: "error"; message: string }; function messageFor(state: CouponState) {  if (state.status === "error") {    return state.message;  }  return state.message;}

Lines 1 to 5 list the four shapes the state can have. Inside the if on line 8, TypeScript knows the status is "error", so line 9 may read message. Line 11 is outside it, where the state could be any of the other three, and TypeScript stops it: Property 'message' does not exist on type '{ status: "typing"; }'. That means the typing shape has no message to read. The impossible state, a message with no error, can't even be written. Typing state and hooks shows how to give useState a type like this.

Step 5: connect the handlers to the state

Last, make each trigger set state. Typing is one line: the box's onChange on line 32 of the overview's form calls setCode(e.target.value). Apply and the server's answer both live in handleSubmit, from lines 13 to 24 of that form:

handleSubmit: one human input and two computer inputsJavaScript
async function handleSubmit(e) {  e.preventDefault();  setStatus("submitting");  setError(null);  try {    await checkCoupon(code);    setStatus("success");  } catch (err) {    setStatus("typing");    setError(err);  }}

Line 1 is the form's onSubmit handler. It's async, because it waits for the server. Line 2 stops the browser's own submit. Lines 3 and 4 handle the human input, Apply: the form moves to Submitting, and any old error goes. Line 6 waits. If the server accepts, line 7 sets "success". If it refuses, checkCoupon throws, and the catch block moves the form back to typing on line 9 and keeps the error on line 10.

Compare it with the by-hand handleSubmit. Neither branch mentions the box, the button, or the messages. Each one says which look comes next, and the JSX already knows what every look shows. Lines 9 and 10 can't forget to turn the box back on, because they don't turn anything on.

Lines 3 and 4 run together, before the await on line 6, so React renders them as one change. Lines 9 and 10 run together after it, and React batches them the same way, as Queueing a series of state updates showed. So the page never shows a half-set state, like Submitting with the old error still under it.

Form actions in React 19

React 19 can also run a function as a form's action, and its useActionState and useFormStatus Hooks keep track of the waiting look for you. They don't change this lesson's way of thinking: you still list the looks and what moves between them. The Form actions lesson covers them. onSubmit with preventDefault, as here, keeps working.

Reviews that load, built with the five steps

The same five steps work for anything that waits, not just forms. Here is a product page's reviews panel. You press a button, the reviews load from a server, and the panel shows them. The pretend server in the api.js tab fails the first time on purpose, as networks sometimes do, and works after that. Press Show reviews, wait, then press Try again:

A reviews panel with five looksPop out in the code editor (opens in a new tab)JSX
import { useState } from "react";import { loadReviews } from "./api.js"; export default function App() {  const [reviews, setReviews] = useState([]);  const [error, setError] = useState(null);  const [status, setStatus] = useState("closed"); // "closed", "loading", or "loaded"   async function handleShow() {    setStatus("loading");    setError(null);    try {      setReviews(await loadReviews());      setStatus("loaded");    } catch (err) {      setStatus("closed");      setError(err);    }  }   if (status === "loading") {    return <p>Loading reviews…</p>;  }  if (status === "loaded") {    return reviews.length === 0 ? (      <p>No reviews yet.</p>    ) : (      <ul>        {reviews.map((review) => (          <li key={review}>{review}</li>        ))}      </ul>    );  }  return (    <>      <button onClick={handleShow}>{error === null ? "Show reviews" : "Try again"}</button>      {error !== null && <p>{error.message}</p>}    </>  );}
ResultLoading…

Here are the five steps, applied to the panel:

  1. The looks: closed (a button), loading, loaded with reviews, loaded with none, and error (a Try again button and the message).
  2. The triggers: a click on the button (human), and the server's answer or failure (computer).
  3. The state: the reviews, the last error, and a status on line 7: "closed", "loading", or "loaded".
  4. What's removed: there is no isEmpty. Line 25 works it out from reviews.length. There is no isError. Lines 37 and 38 check error.
  5. The handler: handleShow on lines 9 to 19 sets "loading", waits, then sets "loaded", or sets "closed" and keeps the error.

Lines 21 to 23 return the loading look, lines 24 to 34 the loaded looks, and lines 35 to 40 the closed and error looks, which share a button whose label depends on error. To see the "no reviews" look, press Edit & run, open the api.js tab, and change line 11 to resolve([]);. Show reviews, then Try again, now ends with No reviews yet..

Each change in these forms has a trigger. Sort these: does a person cause it, or the computer?

Human input or computer input?
  • The shopper types a letter in the coupon box
  • The shopper presses Apply
  • The server answers that the coupon is valid
  • The shopper presses Enter in the box
  • The network drops while the reviews load
  • A two-second timer ends and hides a Copied note
  • The product photo finishes loading
  • The shopper clicks Try again
Try it yourself
0 of 8 correct

Sort each trigger on a shop page: does a person cause it, or does it come from the computer?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "In React, you show and hide elements in event handlers." No. Handlers set state. The JSX describes what each state shows, and React changes the page to match.
  • "A visual state and a state variable are the same thing." No. The coupon form has five looks and three state variables. Empty and Error are worked out from code and error.
  • "More booleans make a form clearer." No. Each boolean doubles the combinations. Five allow 32, and 27 of them are impossible states that a missed setter can reach.
  • "Only clicks and typing change state." No. Computer inputs, like a server's answer or a timer, change it too. They set state in the code that waits for them.
  • "A status needs a library or an enum type." No. A string like "submitting" in useState is enough. TypeScript can list the allowed strings, but plain JavaScript works the same way.
  • "A mock is wasted work." No. The mock's JSX becomes the real component's JSX. You only swap the status prop for state and add the handlers.

Practice exercises

Each exercise shows its code's result under it, or when you press Edit & run. The pretend servers answer after one second. Try a guess first, then use the hints.

Exercise 1 · Warm-upPredict what a mock shows

This mock is rendered with no props. What does its button say? Answer first, then press Edit & run to check.

export default function SaveButton({ status = "saving" }) {
  return (
    <button disabled={status === "saving" || status === "saved"}>
      {status === "saving" ? "Saving…" : status === "saved" ? "Saved" : "Save"}
    </button>
  );
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upAdd the missing looks to a styleguide

    The styleguide shows two of the save button's looks. Add "saved" and "error". How many buttons does the page show now?

    import SaveButton from "./SaveButton.jsx"; const statuses = ["idle", "saving"]; export default function App() {  return (    <>      {statuses.map((status) => (        <section key={status}>          <h4>{status}</h4>          <SaveButton status={status} />        </section>      ))}    </>  );}
    ResultLoading…

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFix the box that locks you out

      Fix the by-hand coupon box so that you can change your code after an error. Test it with TEA5. What line did you add?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const form = document.querySelector("#coupon-form");const input = document.querySelector("#code");const button = document.querySelector("#apply");const checking = document.querySelector("#checking");const errorText = document.querySelector("#error");const success = document.querySelector("#success"); async function handleSubmit(e) {  e.preventDefault();  input.disabled = true;  button.disabled = true;  checking.hidden = false;  errorText.hidden = true;  try {    await checkCoupon(input.value);    form.hidden = true;    success.textContent = "Coupon " + input.value + " applied: 10% off.";    success.hidden = false;  } catch (err) {    checking.hidden = true;    errorText.textContent = err.message;    errorText.hidden = false;    button.disabled = false;  }} function handleInput() {  button.disabled = input.value.length === 0;} form.addEventListener("submit", handleSubmit);input.addEventListener("input", handleInput);
      ResultLoading…

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeReplace two booleans with a status

        Press Subscribe and wait a second: the page says it's subscribing and subscribed at once. Replace the two booleans with one status. After your fix, what does the page show one second after the click?

        import { useState } from "react";import { subscribe } from "./api.js"; export default function App() {  const [isSubscribing, setIsSubscribing] = useState(false);  const [isSubscribed, setIsSubscribed] = useState(false);   async function handleClick() {    setIsSubscribing(true);    await subscribe("asha@shop.com");    setIsSubscribed(true);  }   return (    <>      <button onClick={handleClick} disabled={isSubscribing}>Subscribe</button>      {isSubscribing && <p>Subscribing…</p>}      {isSubscribed && <p>You're subscribed.</p>}    </>  );}
        ResultLoading…

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeAdd the error look

          The pretend server refuses taken@shop.com, but this form has no error look: it waits forever. Add one, so that you can change the email and try again. What does your form say after you press Sign up?

          import { useState } from "react";import { subscribe } from "./api.js"; export default function App() {  const [email, setEmail] = useState("taken@shop.com");  const [status, setStatus] = useState("typing"); // "typing", "sending", or "sent"   async function handleSubmit(e) {    e.preventDefault();    setStatus("sending");    await subscribe(email);    setStatus("sent");  }   if (status === "sent") {    return <p>Thanks! Check {email} for a welcome note.</p>;  }  return (    <form onSubmit={handleSubmit}>      <label>        Email <input value={email} onChange={(e) => setEmail(e.target.value)} disabled={status === "sending"} />      </label>      <button disabled={email.length === 0 || status === "sending"}>Sign up</button>      {status === "sending" && <p>Signing you up…</p>}    </form>  );}
          ResultLoading…

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeRemove the state you can work out

            This form keeps four pieces of state. Two of them can be worked out from the password. Remove them, and keep the form working. How many useState calls are left?

            import { useState } from "react"; export default function App() {  const [password, setPassword] = useState("");  const [isEmpty, setIsEmpty] = useState(true);  const [isTooShort, setIsTooShort] = useState(false);  const [status, setStatus] = useState("typing"); // "typing" or "saved"   function handleChange(e) {    setPassword(e.target.value);    setIsEmpty(e.target.value.length === 0);    setIsTooShort(e.target.value.length > 0 && e.target.value.length < 8);  }   if (status === "saved") {    return <p>Password saved.</p>;  }  return (    <>      <label>        New password <input value={password} onChange={handleChange} />      </label>      <button disabled={isEmpty || isTooShort} onClick={() => setStatus("saved")}>Save</button>      {isTooShort && <p>Use at least 8 characters.</p>}    </>  );}
            ResultLoading…

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each code question, find the status first, then read which conditions are true for it. Press Edit & run on a question's code to check your answer.

              The Reacting to input with state quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Which line describes the coupon box declaratively?

                Choose an answer to see the explanation.

              2. Question 2 of 7With "error" as its default status, what does this mock show?

                Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                export default function CouponForm({ status = "error" }) {
                  if (status === "success") {
                    return <p>Coupon TEA10 applied: 10% off.</p>;
                  }
                  return (
                    <form onSubmit={(e) => e.preventDefault()}>
                      <label>
                        Coupon code <input disabled={status === "submitting"} />
                      </label>
                      <button disabled={status === "empty" || status === "submitting"}>Apply</button>
                      {status === "submitting" && <p>Checking your code…</p>}
                      {status === "error" && <p>That code isn't valid. Check it and try again.</p>}
                    </form>
                  );
                }

                Choose an answer to see the explanation.

              3. Question 3 of 7How many combinations can four boolean state variables be set to?

                Choose an answer to see the explanation.

              4. Question 4 of 7Which of these is a computer input?

                Choose an answer to see the explanation.

              5. Question 5 of 7A like button keeps this state. What's wrong with it?

                Read the code, then predictJavaScript
                const [isLiking, setIsLiking] = useState(false);
                const [isLiked, setIsLiked] = useState(false);
                const [isFailed, setIsFailed] = useState(false);

                Choose an answer to see the explanation.

              6. Question 6 of 7The coupon form's first try kept isEmpty next to code. Why remove it?

                Choose an answer to see the explanation.

              7. Question 7 of 7In the final coupon form, what moves it from Submitting to Error?

                Choose an answer to see the explanation.

              Key takeaways

              • Imperative UI code gives the page commands for each event, and every list of commands can miss a part. Declarative code describes each look once, and React makes the changes.
              • Step 1: list every look the user can see. Draw them with a mock that takes a status prop, or all at once in a living styleguide.
              • Step 2: find the triggers. Human inputs come through event handlers; computer inputs, like a server's answer, come in the code that waits for them. Both set state.
              • Steps 3 and 4: hold the looks in the fewest pieces of state. Replace booleans that clash with one status, and work out anything another piece of state already says.
              • Step 5: make each handler set state to the next look, and nothing else. In TypeScript, a discriminated union can make impossible states impossible to write.

              Remember the one-liner.
              Describe every look once, and let one status choose which the user sees.

              Coming next: Choosing the state structure. It turns the questions from Step 4 into five principles for any state: group it, don't let it contradict itself, and don't repeat, copy, or deeply nest it.

              CompleteFrontend Clear concepts. Working examples.