Skip
Arish's avatar

13. Custom Hooks


Custom Hooks

Custom hooks let you extract component logic into reusable functions. They're just functions that use other hooks.

Creating Custom Hooks

Custom hook names must start with use:

jsx
1// useCounter.js
2import { useState } from 'react'
3
4function useCounter(initialValue = 0) {
5  const [count, setCount] = useState(initialValue)
6  
7  const increment = () => setCount(c => c + 1)
8  const decrement = () => setCount(c => c - 1)
9  const reset = () => setCount(initialValue)
10  
11  return { count, increment, decrement, reset }
12}
13
14// Usage
15function Counter() {
16  const { count, increment, decrement, reset } = useCounter(10)
17  
18  return (
19    <div>
20      <p>{count}</p>
21      <button onClick={increment}>+</button>
22      <button onClick={decrement}>-</button>
23      <button onClick={reset}>Reset</button>
24    </div>
25  )
26}

Common Custom Hooks

useToggle

jsx
1function useToggle(initialValue = false) {
2  const [value, setValue] = useState(initialValue)
3  
4  const toggle = useCallback(() => setValue(v => !v), [])
5  const setTrue = useCallback(() => setValue(true), [])
6  const setFalse = useCallback(() => setValue(false), [])
7  
8  return { value, toggle, setTrue, setFalse }
9}
10
11// Usage
12function Modal() {
13  const { value: isOpen, toggle, setFalse: close } = useToggle()
14  
15  return (
16    <>
17      <button onClick={toggle}>Toggle Modal</button>
18      {isOpen && (
19        <div className="modal">
20          <button onClick={close}>Close</button>
21        </div>
22      )}
23    </>
24  )
25}

useLocalStorage

jsx
1function useLocalStorage(key, initialValue) {
2  const [storedValue, setStoredValue] = useState(() => {
3    try {
4      const item = localStorage.getItem(key)
5      return item ? JSON.parse(item) : initialValue
6    } catch (error) {
7      return initialValue
8    }
9  })
10  
11  const setValue = useCallback((value) => {
12    try {
13      const valueToStore = value instanceof Function ? value(storedValue) : value
14      setStoredValue(valueToStore)
15      localStorage.setItem(key, JSON.stringify(valueToStore))
16    } catch (error) {
17      console.error(error)
18    }
19  }, [key, storedValue])
20  
21  return [storedValue, setValue]
22}
23
24// Usage
25function Settings() {
26  const [theme, setTheme] = useLocalStorage('theme', 'light')
27  
28  return (
29    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
30      Current theme: {theme}
31    </button>
32  )
33}

useFetch

jsx
1function useFetch(url) {
2  const [data, setData] = useState(null)
3  const [loading, setLoading] = useState(true)
4  const [error, setError] = useState(null)
5  
6  useEffect(() => {
7    const controller = new AbortController()
8    
9    const fetchData = async () => {
10      try {
11        setLoading(true)
12        setError(null)
13        
14        const response = await fetch(url, { signal: controller.signal })
15        
16        if (!response.ok) {
17          throw new Error(`HTTP error! status: ${response.status}`)
18        }
19        
20        const json = await response.json()
21        setData(json)
22      } catch (err) {
23        if (err.name !== 'AbortError') {
24          setError(err.message)
25        }
26      } finally {
27        setLoading(false)
28      }
29    }
30    
31    fetchData()
32    
33    return () => controller.abort()
34  }, [url])
35  
36  return { data, loading, error }
37}
38
39// Usage
40function UserProfile({ userId }) {
41  const { data: user, loading, error } = useFetch(`/api/users/${userId}`)
42  
43  if (loading) return <p>Loading...</p>
44  if (error) return <p>Error: {error}</p>
45  
46  return <h1>{user.name}</h1>
47}

useDebounce

jsx
1function useDebounce(value, delay) {
2  const [debouncedValue, setDebouncedValue] = useState(value)
3  
4  useEffect(() => {
5    const timer = setTimeout(() => {
6      setDebouncedValue(value)
7    }, delay)
8    
9    return () => clearTimeout(timer)
10  }, [value, delay])
11  
12  return debouncedValue
13}
14
15// Usage
16function Search() {
17  const [query, setQuery] = useState('')
18  const debouncedQuery = useDebounce(query, 500)
19  
20  useEffect(() => {
21    if (debouncedQuery) {
22      // Perform search
23      searchAPI(debouncedQuery)
24    }
25  }, [debouncedQuery])
26  
27  return (
28    <input 
29      value={query}
30      onChange={e => setQuery(e.target.value)}
31      placeholder="Search..."
32    />
33  )
34}

useWindowSize

jsx
1function useWindowSize() {
2  const [size, setSize] = useState({
3    width: window.innerWidth,
4    height: window.innerHeight
5  })
6  
7  useEffect(() => {
8    const handleResize = () => {
9      setSize({
10        width: window.innerWidth,
11        height: window.innerHeight
12      })
13    }
14    
15    window.addEventListener('resize', handleResize)
16    return () => window.removeEventListener('resize', handleResize)
17  }, [])
18  
19  return size
20}
21
22// Usage
23function ResponsiveComponent() {
24  const { width } = useWindowSize()
25  
26  return (
27    <div>
28      {width < 768 ? <MobileLayout /> : <DesktopLayout />}
29    </div>
30  )
31}

useOnClickOutside

jsx
1function useOnClickOutside(ref, handler) {
2  useEffect(() => {
3    const listener = (event) => {
4      if (!ref.current || ref.current.contains(event.target)) {
5        return
6      }
7      handler(event)
8    }
9    
10    document.addEventListener('mousedown', listener)
11    document.addEventListener('touchstart', listener)
12    
13    return () => {
14      document.removeEventListener('mousedown', listener)
15      document.removeEventListener('touchstart', listener)
16    }
17  }, [ref, handler])
18}
19
20// Usage
21function Dropdown() {
22  const [isOpen, setIsOpen] = useState(false)
23  const ref = useRef(null)
24  
25  useOnClickOutside(ref, () => setIsOpen(false))
26  
27  return (
28    <div ref={ref}>
29      <button onClick={() => setIsOpen(!isOpen)}>Menu</button>
30      {isOpen && <ul className="dropdown">...</ul>}
31    </div>
32  )
33}

useForm

jsx
1function useForm(initialValues, validate) {
2  const [values, setValues] = useState(initialValues)
3  const [errors, setErrors] = useState({})
4  const [touched, setTouched] = useState({})
5  const [isSubmitting, setIsSubmitting] = useState(false)
6  
7  const handleChange = (e) => {
8    const { name, value } = e.target
9    setValues(prev => ({ ...prev, [name]: value }))
10  }
11  
12  const handleBlur = (e) => {
13    const { name } = e.target
14    setTouched(prev => ({ ...prev, [name]: true }))
15    
16    if (validate) {
17      const validationErrors = validate(values)
18      setErrors(validationErrors)
19    }
20  }
21  
22  const handleSubmit = (onSubmit) => async (e) => {
23    e.preventDefault()
24    
25    const validationErrors = validate ? validate(values) : {}
26    setErrors(validationErrors)
27    setTouched(Object.keys(values).reduce((acc, key) => ({ ...acc, [key]: true }), {}))
28    
29    if (Object.keys(validationErrors).length === 0) {
30      setIsSubmitting(true)
31      await onSubmit(values)
32      setIsSubmitting(false)
33    }
34  }
35  
36  const reset = () => {
37    setValues(initialValues)
38    setErrors({})
39    setTouched({})
40  }
41  
42  return {
43    values,
44    errors,
45    touched,
46    isSubmitting,
47    handleChange,
48    handleBlur,
49    handleSubmit,
50    reset
51  }
52}
53
54// Usage
55function LoginForm() {
56  const validate = (values) => {
57    const errors = {}
58    if (!values.email) errors.email = 'Required'
59    if (!values.password) errors.password = 'Required'
60    return errors
61  }
62  
63  const { values, errors, touched, handleChange, handleBlur, handleSubmit } = 
64    useForm({ email: '', password: '' }, validate)
65  
66  return (
67    <form onSubmit={handleSubmit(async (data) => {
68      await loginUser(data)
69    })}>
70      <input
71        name="email"
72        value={values.email}
73        onChange={handleChange}
74        onBlur={handleBlur}
75      />
76      {touched.email && errors.email && <span>{errors.email}</span>}
77      
78      <input
79        name="password"
80        type="password"
81        value={values.password}
82        onChange={handleChange}
83        onBlur={handleBlur}
84      />
85      {touched.password && errors.password && <span>{errors.password}</span>}
86      
87      <button type="submit">Login</button>
88    </form>
89  )
90}

Rules for Custom Hooks

  1. Name must start with use
  2. Can call other hooks
  3. Follow all hook rules (top level only, etc.)
  4. Each component using the hook gets its own state
jsx
1// Each component gets separate state
2function ComponentA() {
3  const counter = useCounter()  // Has its own count
4}
5
6function ComponentB() {
7  const counter = useCounter()  // Has its own count
8}

Custom hooks are the best way to share logic between components!