React Exercises
Three short React drills - custom hook, controlled form, and a tiny state machine.
Three short challenges
EXAMPLE
// 1. Custom hook - usePrevious
import { useEffect, useRef } from 'react';
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => { ref.current = value; }, [value]);
return ref.current;
}
function Counter() {
const [n, setN] = useState(0);
const prev = usePrevious(n);
return <button onClick={() => setN(n + 1)}>n={n}, prev={prev ?? 'none'}</button>;
}
// 2. Controlled form with validation - no library
import { useState } from 'react';
type Errors = Partial<Record<'email' | 'password', string>>;
function Login({ onSubmit }: { onSubmit: (v: { email: string; password: string }) => Promise<void> }) {
const [v, setV] = useState({ email: '', password: '' });
const [errors, setErrors] = useState<Errors>({});
const [busy, setBusy] = useState(false);
const validate = (): Errors => {
const e: Errors = {};
if (!/^[^@]+@[^@]+\.[^@]+$/.test(v.email)) e.email = 'Enter a valid email';
if (v.password.length < 8) e.password = 'At least 8 characters';
return e;
};
const submit = async (ev: React.FormEvent) => {
ev.preventDefault();
const e = validate();
setErrors(e);
if (Object.keys(e).length) return;
setBusy(true);
try { await onSubmit(v); } finally { setBusy(false); }
};
return (
<form onSubmit={submit} className='space-y-2'>
<label>
Email
<input value={v.email} onChange={(e) => setV({ ...v, email: e.target.value })} />
{errors.email && <span className='text-red-500'>{errors.email}</span>}
</label>
<label>
Password
<input type='password' value={v.password} onChange={(e) => setV({ ...v, password: e.target.value })} />
{errors.password && <span className='text-red-500'>{errors.password}</span>}
</label>
<button disabled={busy}>Sign in</button>
</form>
);
}
// 3. Tiny state machine with useReducer
type State = { status: 'idle' | 'loading' | 'success' | 'error'; data?: any; error?: string };
type Action =
| { type: 'fetch' }
| { type: 'success'; data: any }
| { type: 'error'; error: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'fetch': return { status: 'loading' };
case 'success': return { status: 'success', data: action.data };
case 'error': return { status: 'error', error: action.error };
}
}
function User({ id }: { id: string }) {
const [state, dispatch] = useReducer(reducer, { status: 'idle' });
useEffect(() => {
dispatch({ type: 'fetch' });
fetch(\`/api/users/${id}\`)
.then((r) => r.ok ? r.json() : Promise.reject(new Error(\`${r.status}\`)))
.then((data) => dispatch({ type: 'success', data }))
.catch((e) => dispatch({ type: 'error', error: String(e) }));
}, [id]);
if (state.status === 'loading') return <p>loading</p>;
if (state.status === 'error') return <p>error: {state.error}</p>;
if (state.status === 'success') return <p>{state.data.name}</p>;
return null;
}
Why it matters
Custom hooks for reuse, controlled forms for clarity, state machines for explicit transitions. These three patterns let you scale beyond a couple of useState calls without reaching for a library.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Fill in the blank.
function Greet(____) { return <h1>Hi, {props.name}</h1>; }
Try it Yourself »
Discussion
Loading…