Skip to content
Laviyx Learn

Try

React Forms and Hooks Practice

Practise controlled forms, updating arrays in state, lifting state, useEffect with cleanup, useRef, useReducer, context and custom hooks. Each solution runs live.

Intermediate6 problemsUpdated

Review first: Forms and Controlled Inputs, Updating Objects and Arrays in State, Sharing State Between Components, Effects with useEffect, Refs with useRef, Managing Complex State with useReducer, Context: Passing Data Deeply, Custom Hooks

  1. 1.Guest list

    Intermediate

    Build a guest list: a form to add a name (ignoring empty names and duplicates, case-insensitively), a list of guests each with a remove button, and a count.

    Show hint

    Check duplicates with guests.some((g) => g.toLowerCase() === name.toLowerCase()). Add with spread, remove with filter.

    Show solution
    JSX
    import { useState } from "react";
    
    export default function App() {
      const [guests, setGuests] = useState(["Asha", "Ravi"]);
      const [name, setName] = useState("");
      const [message, setMessage] = useState("");
    
      function add(e) {
        e.preventDefault();
        const trimmed = name.trim();
        if (!trimmed) return;
        if (guests.some((g) => g.toLowerCase() === trimmed.toLowerCase())) {
          setMessage(`${trimmed} is already on the list`);
          return;
        }
        setGuests([...guests, trimmed]);
        setName("");
        setMessage("");
      }
    
      return (
        <form onSubmit={add}>
          <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Guest name" />
          <button type="submit">Add</button>
          {message && <p style={{ color: "crimson" }}>{message}</p>}
          <ul>
            {guests.map((g) => (
              <li key={g}>
                {g} <button type="button" onClick={() => setGuests(guests.filter((x) => x !== g))}>Remove</button>
              </li>
            ))}
          </ul>
          <p>{guests.length} guests</p>
        </form>
      );
    }
    Result

    The remove buttons use type="button" so they do not submit the form. Names are unique, so they work as keys.

  2. 2.Lifted filter state

    Intermediate

    Build a CategoryPicker component (buttons for "All", "Fruit", "Vegetable") and a ProduceList component in the same parent. Choosing a category in the picker filters the list. Keep the state in the parent.

    Show hint

    The parent holds category and passes category and onChange to the picker, and the filtered items to the list.

    Show solution
    JSX
    import { useState } from "react";
    
    const PRODUCE = [
      { name: "Mango", type: "Fruit" },
      { name: "Spinach", type: "Vegetable" },
      { name: "Banana", type: "Fruit" },
      { name: "Carrot", type: "Vegetable" },
    ];
    
    function CategoryPicker({ category, onChange }) {
      return ["All", "Fruit", "Vegetable"].map((c) => (
        <button key={c} onClick={() => onChange(c)} style={{ fontWeight: c === category ? "bold" : "normal", marginRight: 4 }}>
          {c}
        </button>
      ));
    }
    
    function ProduceList({ items }) {
      return <ul>{items.map((p) => <li key={p.name}>{p.name}</li>)}</ul>;
    }
    
    export default function App() {
      const [category, setCategory] = useState("All");
      const items = PRODUCE.filter((p) => category === "All" || p.type === category);
      return (
        <div>
          <CategoryPicker category={category} onChange={setCategory} />
          <ProduceList items={items} />
        </div>
      );
    }
    Result

    Both children depend on the category, so it lives in their common parent: data flows down as props, changes flow up through onChange.

  3. 3.Live clock with cleanup

    Intermediate

    Show the current time (hours:minutes:seconds) updating every second, with a button to hide and show the clock. Make sure the timer stops when the clock is hidden.

    Show hint

    Put setInterval in a useEffect with an empty dependency array inside a Clock component, and return a cleanup that calls clearInterval.

    Show solution
    JSX
    import { useEffect, useState } from "react";
    
    function Clock() {
      const [now, setNow] = useState(() => new Date());
      useEffect(() => {
        const id = setInterval(() => setNow(new Date()), 1000);
        return () => clearInterval(id);
      }, []);
      return <p style={{ fontSize: 24, fontFamily: "monospace" }}>{now.toLocaleTimeString("en-GB")}</p>;
    }
    
    export default function App() {
      const [visible, setVisible] = useState(true);
      return (
        <div>
          <button onClick={() => setVisible(!visible)}>{visible ? "Hide" : "Show"} clock</button>
          {visible && <Clock />}
        </div>
      );
    }
    Result

    Hiding the clock unmounts Clock, which runs the cleanup and clears the interval. "en-GB" formats the time as a 24-hour clock.

  4. 4.Move focus with refs

    Intermediate

    Build a 4-box OTP-style input: typing a digit in one box moves the cursor to the next box automatically.

    Show hint

    Keep an array of refs with useRef([]) and ref={(el) => (refs.current[i] = el)}. In onChange, focus refs.current[i + 1].

    Show solution
    JSX
    import { useRef, useState } from "react";
    
    export default function App() {
      const [digits, setDigits] = useState(["", "", "", ""]);
      const refs = useRef([]);
    
      function handleChange(i, value) {
        const digit = value.replace(/\D/g, "").slice(-1);
        setDigits(digits.map((d, j) => (j === i ? digit : d)));
        if (digit && i < 3) refs.current[i + 1].focus();
      }
    
      return (
        <div>
          {digits.map((d, i) => (
            <input
              key={i}
              ref={(el) => (refs.current[i] = el)}
              value={d}
              onChange={(e) => handleChange(i, e.target.value)}
              inputMode="numeric"
              style={{ width: 32, textAlign: "center", fontSize: 20, marginRight: 4 }}
            />
          ))}
          <p>Code: {digits.join("") || "—"}</p>
        </div>
      );
    }
    Result

    A callback ref stores each input in an array. Focusing is something React has no prop for, so it is a good use of refs. The boxes never move, so index keys are safe here.

  5. 5.Poll with useReducer

    Intermediate

    Build a poll with three options. Use useReducer with actions "voted" (with the option) and "reset". Show the votes and percentage for each option.

    Show hint

    State: { Tea: 0, Coffee: 0, Juice: 0 }. For "voted": { ...state, [action.option]: state[action.option] + 1 }.

    Show solution
    JSX
    import { useReducer } from "react";
    
    const initial = { Tea: 0, Coffee: 0, Juice: 0 };
    
    function pollReducer(state, action) {
      switch (action.type) {
        case "voted":
          return { ...state, [action.option]: state[action.option] + 1 };
        case "reset":
          return initial;
        default:
          throw new Error(`Unknown action: ${action.type}`);
      }
    }
    
    export default function App() {
      const [votes, dispatch] = useReducer(pollReducer, initial);
      const total = Object.values(votes).reduce((a, b) => a + b, 0);
      return (
        <div>
          {Object.entries(votes).map(([option, count]) => (
            <p key={option}>
              <button onClick={() => dispatch({ type: "voted", option })}>{option}</button>{" "}
              {count} {count === 1 ? "vote" : "votes"} ({total ? Math.round((count / total) * 100) : 0}%)
            </p>
          ))}
          <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
        </div>
      );
    }
    Result

    The reducer holds all vote logic; the total and percentages are derived during rendering.

  6. 6.Theme with context and a custom hook

    Intermediate

    Create a theme context with a ThemeProvider component and a useTheme hook that returns { dark, toggle }. Use it from two nested components: a button that toggles the theme and a panel that changes colour.

    Show hint

    The provider keeps dark in state and provides { dark, toggle }. useTheme calls useContext and throws if the value is missing.

    Show solution
    JSX
    import { createContext, useContext, useState } from "react";
    
    const ThemeContext = createContext(null);
    
    function ThemeProvider({ children }) {
      const [dark, setDark] = useState(false);
      return <ThemeContext value={{ dark, toggle: () => setDark((d) => !d) }}>{children}</ThemeContext>;
    }
    
    function useTheme() {
      const value = useContext(ThemeContext);
      if (!value) throw new Error("useTheme must be used inside <ThemeProvider>");
      return value;
    }
    
    function ToggleButton() {
      const { dark, toggle } = useTheme();
      return <button onClick={toggle}>Switch to {dark ? "light" : "dark"}</button>;
    }
    
    function Panel() {
      const { dark } = useTheme();
      return (
        <div style={{ background: dark ? "#222" : "#f1f3f5", color: dark ? "#eee" : "#222", padding: 12, marginTop: 8 }}>
          {dark ? "Dark" : "Light"} theme
        </div>
      );
    }
    
    export default function App() {
      return (
        <ThemeProvider>
          <ToggleButton />
          <Panel />
        </ThemeProvider>
      );
    }
    Result

    The two components never pass props to each other; both read the same context through the hook.