Skip
Arish's avatar

39. Hook and State Challenges


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!