Skip to content

React Tutorial Practice and Reference Exercise

React Practice Exercises: State, Events and Hooks

Lesson 150 of 156 2 min read By Deepak Paswan Updated Oct 10, 2026

Runnable lesson · press Try it on the examples to edit and run them here.

These exercises need useState, events, effects, refs and a custom hook. Solve each one first, then compare.

Exercise: 1. Show and hide a password

A password input with a “Show” button that toggles between showing the text and hiding it. The button label changes to “Hide” while visible.

Hint: type={visible ? "text" : "password"}. —solution

App.jsx
JSX
function App() {
  const [value, setValue] = React.useState("");
  const [visible, setVisible] = React.useState(false);

  return (
    <div>
      <label>
        Password{" "}
        <input aria-label="Password" type={visible ? "text" : "password"} value={value} onChange={(e) => setValue(e.target.value)} />
      </label>{" "}
      <button type="button" onClick={() => setVisible(!visible)} aria-pressed={visible}>{visible ? "Hide" : "Show"}</button>
    </div>
  );
}

Exercise: 2. A temperature slider

Use <input type="range"> from 0 to 40. Show the value in degrees and a word: “Cold” below 10, “Warm” from 10 to 25, “Hot” above 25. The word must not be stored in state.

Hint: Derived State. —solution

App.jsx
JSX
function App() {
  const [temp, setTemp] = React.useState(15);
  const feel = temp < 10 ? "Cold" : temp <= 25 ? "Warm" : "Hot"; // derived, not state

  return (
    <div>
      <label>
        Temperature{" "}
        <input type="range" min="0" max="40" value={temp} onChange={(e) => setTemp(Number(e.target.value))} aria-label="Temperature" />
      </label>
      <p>{temp} degrees: {feel}</p>
    </div>
  );
}

Exercise: 3. Live profile preview

Two inputs (name and bio) and, beside them, a preview card showing what is typed. If the name is empty, the preview shows “Your name”. Use one state object and one change handler.

Hint: Controlled Forms. —solution

App.jsx
JSX
function App() {
  const [profile, setProfile] = React.useState({ name: "", bio: "" });
  const handleChange = (e) => setProfile({ ...profile, [e.target.name]: e.target.value });

  return (
    <div style={{ display: "flex", gap: 24, flexWrap: "wrap" }}>
      <form onSubmit={(e) => e.preventDefault()} style={{ display: "grid", gap: 8 }}>
        <input name="name" aria-label="name" placeholder="Name" value={profile.name} onChange={handleChange} />
        <textarea name="bio" aria-label="bio" placeholder="Bio" value={profile.bio} onChange={handleChange} />
      </form>
      <aside style={{ border: "1px solid #ccc", borderRadius: 8, padding: 12, minWidth: 180 }}>
        <h3 style={{ margin: 0 }}>{profile.name || "Your name"}</h3>
        <p style={{ margin: 0 }}>{profile.bio}</p>
      </aside>
    </div>
  );
}

Exercise: 4. A shopping list with removal

Add items from an input and remove a single item with a button next to it. Items can repeat, so each needs its own id. Show a message when the list is empty.

Hint: crypto.randomUUID() in the handler, filter to remove (Updating Arrays in State). —solution

App.jsx
JSX
function App() {
  const [items, setItems] = React.useState([]);
  const [text, setText] = React.useState("");

  function add(e) {
    e.preventDefault();
    if (!text.trim()) return;
    setItems([...items, { id: crypto.randomUUID(), name: text.trim() }]);
    setText("");
  }

  return (
    <div>
      <form onSubmit={add}>
        <input aria-label="Item" value={text} onChange={(e) => setText(e.target.value)} />
        <button type="submit">Add</button>
      </form>
      {items.length === 0 ? (
        <p>List is empty</p>
      ) : (
        <ul>
          {items.map((item) => (
            <li key={item.id}>
              {item.name}{" "}
              <button aria-label={"Remove " + item.name} onClick={() => setItems(items.filter((i) => i.id !== item.id))}>Remove</button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

Both items are called “Milk”, yet each button removes only its own row, because the ids are different. That is why ids matter more than names.

Exercise: 5. A countdown timer

Count down from 5 to 0 with an interval that stops at 0. A Restart button starts it again. Clean up the timer.

Hint: useEffect with setInterval, an updater function and a cleanup (Cleanup Functions). —solution

App.jsx
JSX
function App() {
  const [left, setLeft] = React.useState(5);
  const [run, setRun] = React.useState(0); // changing this restarts the effect

  React.useEffect(() => {
    setLeft(5);
    const id = setInterval(() => {
      setLeft((s) => {
        if (s <= 1) { clearInterval(id); return 0; }
        return s - 1;
      });
    }, 1000);
    return () => clearInterval(id);
  }, [run]);

  return (
    <div>
      <h3>Countdown</h3>
      <p>Remaining: {left}</p>
      {left === 0 && <p role="status">Time is up</p>}
      <button onClick={() => setRun(run + 1)}>Restart</button>
    </div>
  );
}

Exercise: 6. Loading, error and retry

Write loadQuote() that returns a promise: it fails the first time and succeeds afterwards. Show “Loading…”, then an error with a Try again button, then the quote.

Hint: a status state and an attempt counter (Error States). —solution

App.jsx
JSX
let calls = 0;
function loadQuote() {
  calls += 1;
  return new Promise((resolve, reject) =>
    setTimeout(() => (calls === 1 ? reject(new Error("Network error")) : resolve("Simplicity is the soul of efficiency.")), 500)
  );
}

function App() {
  const [state, setState] = React.useState({ status: "loading" });
  const [attempt, setAttempt] = React.useState(0);

  React.useEffect(() => {
    let ignore = false;
    setState({ status: "loading" });
    loadQuote()
      .then((quote) => { if (!ignore) setState({ status: "success", quote }); })
      .catch((e) => { if (!ignore) setState({ status: "error", message: e.message }); });
    return () => { ignore = true; };
  }, [attempt]);

  if (state.status === "loading") return <p>Loading...</p>;
  if (state.status === "error") {
    return (
      <div role="alert">
        <p>Could not load the quote ({state.message}).</p>
        <button onClick={() => setAttempt(attempt + 1)}>Try again</button>
      </div>
    );
  }
  return <blockquote>{state.quote}</blockquote>;
}

Exercise: 7. Focus and clear

An input with a “Clear and focus” button that empties the input and puts the cursor back in it. Use a ref.

Hint: useRef. —solution

App.jsx
JSX
function App() {
  const [text, setText] = React.useState("");
  const inputRef = React.useRef(null);

  function clear() {
    setText("");
    inputRef.current.focus();
  }

  return (
    <div>
      <input ref={inputRef} aria-label="Notes" value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={clear}>Clear and focus</button>
    </div>
  );
}

Exercise: 8. A useCounter hook

Write useCounter(initial, { min, max }) that returns { count, increment, decrement, reset } and never leaves the range. Use it for two independent counters.

Hint: Custom Hooks. —solution

App.jsx
JSX
function useCounter(initial = 0, { min = -Infinity, max = Infinity } = {}) {
  const [count, setCount] = React.useState(initial);
  const clamp = (n) => Math.min(max, Math.max(min, n));
  return {
    count,
    increment: () => setCount((c) => clamp(c + 1)),
    decrement: () => setCount((c) => clamp(c - 1)),
    reset: () => setCount(initial),
  };
}

function App() {
  const apples = useCounter(0, { min: 0, max: 5 });
  const pears = useCounter(3, { min: 0, max: 10 });

  return (
    <div>
      <p>Apples: {apples.count}</p>
      <button onClick={apples.increment}>Add apple</button>
      <p>Pears: {pears.count}</p>
      <button onClick={pears.increment}>Add pear</button>
    </div>
  );
}

Each call to useCounter has its own state, so the two counters do not affect each other.

Next

Move on to the debugging challenges: Debugging Challenges.