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
- Name must start with
use - Can call other hooks
- Follow all hook rules (top level only, etc.)
- 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!
