Hook and State Challenges
Practice building custom hooks and managing complex state.
Challenge 1: useLocalStorage Hook
Create a hook that syncs state with localStorage.
Requirements:
- Persist state across page reloads
- Handle JSON serialization
- Support lazy initial state
Solution:
jsx
1function useLocalStorage(key, initialValue) {
2 // Lazy initialization
3 const [storedValue, setStoredValue] = useState(() => {
4 try {
5 const item = window.localStorage.getItem(key)
6 return item ? JSON.parse(item) : initialValue
7 } catch (error) {
8 console.error(error)
9 return initialValue
10 }
11 })
12
13 // Update localStorage when state changes
14 const setValue = useCallback((value) => {
15 try {
16 // Allow function updates
17 const valueToStore = value instanceof Function ? value(storedValue) : value
18 setStoredValue(valueToStore)
19 window.localStorage.setItem(key, JSON.stringify(valueToStore))
20 } catch (error) {
21 console.error(error)
22 }
23 }, [key, storedValue])
24
25 // Remove from storage
26 const removeValue = useCallback(() => {
27 try {
28 window.localStorage.removeItem(key)
29 setStoredValue(initialValue)
30 } catch (error) {
31 console.error(error)
32 }
33 }, [key, initialValue])
34
35 return [storedValue, setValue, removeValue]
36}
37
38// Usage
39function App() {
40 const [theme, setTheme] = useLocalStorage('theme', 'light')
41 const [user, setUser, removeUser] = useLocalStorage('user', null)
42
43 return (
44 <div>
45 <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
46 Toggle Theme: {theme}
47 </button>
48 </div>
49 )
50}Challenge 2: useFetch Hook with Caching
Build a data fetching hook with caching.
Solution:
jsx
1const cache = new Map()
2
3function useFetch(url, options = {}) {
4 const { cacheTime = 60000 } = options
5 const [state, setState] = useState({
6 data: cache.get(url)?.data || null,
7 loading: !cache.get(url),
8 error: null
9 })
10
11 useEffect(() => {
12 const controller = new AbortController()
13
14 const fetchData = async () => {
15 // Check cache
16 const cached = cache.get(url)
17 if (cached && Date.now() - cached.timestamp < cacheTime) {
18 setState({ data: cached.data, loading: false, error: null })
19 return
20 }
21
22 setState(prev => ({ ...prev, loading: true, error: null }))
23
24 try {
25 const response = await fetch(url, { signal: controller.signal })
26 if (!response.ok) throw new Error(`HTTP ${response.status}`)
27
28 const data = await response.json()
29
30 // Update cache
31 cache.set(url, { data, timestamp: Date.now() })
32 setState({ data, loading: false, error: null })
33 } catch (error) {
34 if (error.name !== 'AbortError') {
35 setState(prev => ({ ...prev, loading: false, error: error.message }))
36 }
37 }
38 }
39
40 fetchData()
41
42 return () => controller.abort()
43 }, [url, cacheTime])
44
45 const refetch = useCallback(() => {
46 cache.delete(url)
47 setState(prev => ({ ...prev, loading: true }))
48 }, [url])
49
50 return { ...state, refetch }
51}
52
53// Usage
54function UserProfile({ userId }) {
55 const { data, loading, error, refetch } = useFetch(`/api/users/${userId}`)
56
57 if (loading) return <Spinner />
58 if (error) return <Error message={error} onRetry={refetch} />
59
60 return <Profile user={data} />
61}Challenge 3: useDebounce and useThrottle
Implement debounce and throttle hooks.
Solution:
jsx
1// Debounce - delays execution until pause in calls
2function useDebounce(value, delay) {
3 const [debouncedValue, setDebouncedValue] = useState(value)
4
5 useEffect(() => {
6 const timer = setTimeout(() => {
7 setDebouncedValue(value)
8 }, delay)
9
10 return () => clearTimeout(timer)
11 }, [value, delay])
12
13 return debouncedValue
14}
15
16// Debounced callback
17function useDebouncedCallback(callback, delay) {
18 const callbackRef = useRef(callback)
19 callbackRef.current = callback
20
21 return useMemo(
22 () => debounce((...args) => callbackRef.current(...args), delay),
23 [delay]
24 )
25}
26
27// Throttle - limits execution to once per interval
28function useThrottle(value, interval) {
29 const [throttledValue, setThrottledValue] = useState(value)
30 const lastUpdated = useRef(Date.now())
31
32 useEffect(() => {
33 const now = Date.now()
34
35 if (now >= lastUpdated.current + interval) {
36 lastUpdated.current = now
37 setThrottledValue(value)
38 } else {
39 const timer = setTimeout(() => {
40 lastUpdated.current = Date.now()
41 setThrottledValue(value)
42 }, interval - (now - lastUpdated.current))
43
44 return () => clearTimeout(timer)
45 }
46 }, [value, interval])
47
48 return throttledValue
49}
50
51// Usage: Debounced search
52function Search() {
53 const [query, setQuery] = useState('')
54 const debouncedQuery = useDebounce(query, 500)
55
56 useEffect(() => {
57 if (debouncedQuery) {
58 searchAPI(debouncedQuery)
59 }
60 }, [debouncedQuery])
61
62 return <input value={query} onChange={e => setQuery(e.target.value)} />
63}Challenge 4: Undo/Redo State Management
Implement state with undo/redo functionality.
Solution:
jsx
1function useUndoRedo(initialState) {
2 const [history, setHistory] = useState({
3 past: [],
4 present: initialState,
5 future: []
6 })
7
8 const canUndo = history.past.length > 0
9 const canRedo = history.future.length > 0
10
11 const set = useCallback((newPresent) => {
12 setHistory(({ past, present }) => ({
13 past: [...past, present],
14 present: typeof newPresent === 'function' ? newPresent(present) : newPresent,
15 future: []
16 }))
17 }, [])
18
19 const undo = useCallback(() => {
20 setHistory(({ past, present, future }) => {
21 if (past.length === 0) return { past, present, future }
22
23 const previous = past[past.length - 1]
24 const newPast = past.slice(0, -1)
25
26 return {
27 past: newPast,
28 present: previous,
29 future: [present, ...future]
30 }
31 })
32 }, [])
33
34 const redo = useCallback(() => {
35 setHistory(({ past, present, future }) => {
36 if (future.length === 0) return { past, present, future }
37
38 const next = future[0]
39 const newFuture = future.slice(1)
40
41 return {
42 past: [...past, present],
43 present: next,
44 future: newFuture
45 }
46 })
47 }, [])
48
49 const reset = useCallback(() => {
50 setHistory({
51 past: [],
52 present: initialState,
53 future: []
54 })
55 }, [initialState])
56
57 return {
58 state: history.present,
59 set,
60 undo,
61 redo,
62 reset,
63 canUndo,
64 canRedo
65 }
66}
67
68// Usage: Text editor with undo/redo
69function Editor() {
70 const { state, set, undo, redo, canUndo, canRedo } = useUndoRedo('')
71
72 return (
73 <div>
74 <div className="flex gap-2 mb-2">
75 <button onClick={undo} disabled={!canUndo}>Undo</button>
76 <button onClick={redo} disabled={!canRedo}>Redo</button>
77 </div>
78 <textarea
79 value={state}
80 onChange={e => set(e.target.value)}
81 rows={10}
82 />
83 </div>
84 )
85}Challenge 5: Form State Machine
Build a form with state machine pattern.
Solution:
jsx
1const formMachine = {
2 idle: {
3 SUBMIT: 'validating'
4 },
5 validating: {
6 VALID: 'submitting',
7 INVALID: 'error'
8 },
9 submitting: {
10 SUCCESS: 'success',
11 FAILURE: 'error'
12 },
13 success: {
14 RESET: 'idle'
15 },
16 error: {
17 SUBMIT: 'validating',
18 RESET: 'idle'
19 }
20}
21
22function useFormMachine(initialState = 'idle') {
23 const [state, setState] = useState(initialState)
24
25 const transition = useCallback((event) => {
26 const nextState = formMachine[state]?.[event]
27 if (nextState) {
28 setState(nextState)
29 }
30 }, [state])
31
32 const is = useCallback((checkState) => state === checkState, [state])
33
34 return { state, transition, is }
35}
36
37// Usage
38function ContactForm() {
39 const { state, transition, is } = useFormMachine()
40 const [formData, setFormData] = useState({ name: '', email: '' })
41 const [errors, setErrors] = useState({})
42
43 const validate = () => {
44 const newErrors = {}
45 if (!formData.name) newErrors.name = 'Name required'
46 if (!formData.email) newErrors.email = 'Email required'
47 setErrors(newErrors)
48 return Object.keys(newErrors).length === 0
49 }
50
51 const handleSubmit = async (e) => {
52 e.preventDefault()
53 transition('SUBMIT')
54
55 if (!validate()) {
56 transition('INVALID')
57 return
58 }
59
60 transition('VALID')
61
62 try {
63 await submitForm(formData)
64 transition('SUCCESS')
65 } catch {
66 transition('FAILURE')
67 }
68 }
69
70 return (
71 <form onSubmit={handleSubmit}>
72 <input
73 value={formData.name}
74 onChange={e => setFormData({ ...formData, name: e.target.value })}
75 disabled={is('submitting')}
76 />
77 {errors.name && <span>{errors.name}</span>}
78
79 <button type="submit" disabled={is('submitting')}>
80 {is('submitting') ? 'Submitting...' : 'Submit'}
81 </button>
82
83 {is('success') && <p>Form submitted successfully!</p>}
84 {is('error') && <p>An error occurred. Please try again.</p>}
85 </form>
86 )
87}These challenges help master React state management!
