Content
Difficulty: Medium Concepts: useReducer, reducer function shape, actions and dispatch, modelling form state
Take the controlled form below — written with multiple useState calls — and refactor it to use a single useReducer.
Starting point — paste this into a new component file:
import { useState } from 'react';
export default function AddConcertForm() {
const [artist, setArtist] = useState('');
const [venue, setVenue] = useState('');
const [date, setDate] = useState('');
const [ticketPrice, setTicketPrice] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [submitted, setSubmitted] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setIsSubmitting(true);
setError(null);
try {
// Simulate an API call
await new Promise((resolve) => setTimeout(resolve, 1000));
if (Math.random() < 0.3) throw new Error('Server error — try again');
setSubmitted(true);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setIsSubmitting(false);
}
}
if (submitted) {
return <p>Concert added!</p>;
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="artist">Artist</label>
<input
id="artist"
value={artist}
onChange={(e) => setArtist(e.target.value)}
/>
</div>
<div>
<label htmlFor="venue">Venue</label>
<input
id="venue"
value={venue}
onChange={(e) => setVenue(e.target.value)}
/>
</div>
<div>
<label htmlFor="date">Date</label>
<input
id="date"
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
/>
</div>
<div>
<label htmlFor="ticketPrice">Ticket Price</label>
<input
id="ticketPrice"
type="number"
value={ticketPrice}
onChange={(e) => setTicketPrice(e.target.value)}
/>
</div>
{error && <p role="alert">{error}</p>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Saving...' : 'Add Concert'}
</button>
</form>
);
}
Requirements:
State type that combines all six fields (artist, venue, date, ticketPrice, isSubmitting, error) and a submitted flag into a single objectAction type as a TypeScript discriminated union with at least four action types: UPDATE_FIELD, SUBMIT_START, SUBMIT_SUCCESS, and SUBMIT_ERRORreducer function outside the component that handles all four action types and returns a new state object without mutating the existing oneuseState calls with a single useReducer(reducer, initialState)UPDATE_FIELD action should carry a field name and a value in its payload, so a single handleFieldChange function can handle all four inputsSUBMIT_START before the async call, SUBMIT_SUCCESS on success, and SUBMIT_ERROR with the error message on failure — no setIsSubmitting, setError, or setSubmitted calls should remainHints:
State type and initialState before touching anything else — getting the shape right first makes the reducer straightforward to writeUPDATE_FIELD, the payload can be { field: keyof State; value: string } — this lets you use computed property syntax in the reducer: { ...state, [action.payload.field]: action.payload.value }Action looks like: type Action = | { type: 'UPDATE_FIELD'; payload: { field: keyof State; value: string } } | { type: 'SUBMIT_START' } | ...switch statement is missing a case — treat that as a helpful reminder, not an obstacledefault: return state at the end of your switchDifficulty: Easy Concepts: createContext, useContext, Provider, consuming context in a child component
Build a small app with a theme toggle. The toggle lives in one component, and the current theme is displayed in another — without passing props between them.
Requirements:
ThemeContext using createContext that holds an object with two properties: theme (a string: 'light' or 'dark') and setTheme (the setter function)ThemeProvider component that owns a useState for the current theme and wraps its children in the context ProviderThemeToggle component that reads theme and setTheme from context using useContext and renders a button that switches between 'light' and 'dark'ThemeDisplay component that reads theme from context and renders a <p> saying "Current theme: light" or "Current theme: dark"App.tsx, wrap both components in <ThemeProvider> and render them. Neither component should receive any propsHints:
value prop is what components receive when they call useContext. If value changes, every consumer re-renderscreateContext needs a default value — since the default is rarely used in practice, an empty object cast to the right type is fine: createContext({} as { theme: string; setTheme: (t: string) => void })ThemeToggle and ThemeDisplay call useContext(ThemeContext) — they just use different parts of the returned objectDifficulty: Easy Concepts: Error boundaries, fallback UI, what gets caught and what doesn't
Add an Error Boundary around a component that throws during rendering, so the rest of the page stays visible.
Requirements:
react-error-boundary: npm install react-error-boundaryBrokenComponent that always throws an error when rendered: throw new Error('Something went wrong')error and resetErrorBoundary as props and renders a short message, the error.message, and a "Try again" button that calls resetErrorBoundaryApp.tsx, render <BrokenComponent /> wrapped in an <ErrorBoundary> with your fallback componentHints:
ErrorBoundary and your fallback from the right places: import { ErrorBoundary } from 'react-error-boundary'<ErrorBoundary FallbackComponent={YourFallback}>BrokenComponent always throws — that's expected and shows the boundary resetting itselfDifficulty: Easy Concepts: useForm, register, handleSubmit, formState errors, validation rules
Build a simple contact form from scratch using React Hook Form. No useState, no onChange handlers — just register, handleSubmit, and errors.
Requirements:
npm install react-hook-formContactForm component that uses useForm with defaultValues for all fieldsname (text input), email (text input), and message (textarea)name: requiredemail: required, with a pattern rule for a valid email addressmessage: required, minimum 20 characterserrors.fieldName?.messageconsole.log(data) and call reset() to clear the fieldsisSubmitting is trueHints:
register onto each input: <input {...register('name', { required: 'Name is required' })} />textarea works exactly like input with React Hook Form — just use <textarea {...register('message', ...)} />isSubmitting from formState: const { register, handleSubmit, reset, formState: { errors, isSubmitting } } = useForm()pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: 'Enter a valid email' }isSubmitting in action, make your onSubmit function async and add await new Promise(r => setTimeout(r, 1500)) before calling reset()The HackYourFuture curriculum is licensed under CC BY-NC-SA 4.0 *https://hackyourfuture.net/*

Built with ❤️ by the HackYourFuture community · Thank you, contributors
Found a mistake or have a suggestion? Let us know in the feedback form.