During our cosmic journey through React, we have reached a point where the basic elements of our spaceship are well known to us. Now it is time to learn how to create our own specialized modules - custom hooks. These are what allow us to achieve a higher level of abstraction and reuse logic across components.
Custom hooks are JavaScript functions that use built-in React hooks (like
useState, useEffect, useContext, etc.) to encapsulate and share logic between components. This is one of the most powerful mechanisms in React, allowing you to:Creating your own hooks follows a few simple rules:
useLocalStorage, useFetch, useSpaceshipStatusLet's start by creating a simple custom hook that manages form state:
1function useFormInput(initialValue) {
2 const [value, setValue] = useState(initialValue);
3
4 function handleChange(e) {
5 setValue(e.target.value);
6 }
7
8 return {
9 value,
10 onChange: handleChange
11 };
12}
13
14// Usage
15function LoginForm() {
16 const username = useFormInput('');
17 const password = useFormInput('');
18
19 function handleSubmit(e) {
20 e.preventDefault();
21 console.log('Login:', username.value, 'Password:', password.value);
22 }
23
24 return (
25 <form onSubmit={handleSubmit}>
26 <div>
27 <label>Username:</label>
28 <input type="text" {...username} />
29 </div>
30 <div>
31 <label>Password:</label>
32 <input type="password" {...password} />
33 </div>
34 <button type="submit">Login</button>
35 </form>
36 );
37}Our
useFormInput hook encapsulates the logic for storing and updating a form field value. It returns an object with the value and a change handler function, which we can spread directly as props on an input component.Let's look at some commonly used custom hooks that can significantly simplify your work.
A hook for storing and synchronizing state with localStorage:
1function useLocalStorage(key, initialValue) {
2 // State to store our value
3 const [storedValue, setStoredValue] = useState(() => {
4 try {
5 // Get from localStorage by key
6 const item = window.localStorage.getItem(key);
7 // Parse stored json or return initialValue
8 return item ? JSON.parse(item) : initialValue;
9 } catch (error) {
10 // If an error occurs, return initialValue
11 console.log(error);
12 return initialValue;
13 }
14 });
15
16 // Return a function that saves to both useState and localStorage
17 const setValue = value => {
18 try {
19 // Allow value to be a function, just like useState
20 const valueToStore =
21 value instanceof Function ? value(storedValue) : value;
22 // Save to state
23 setStoredValue(valueToStore);
24 // Save to localStorage
25 window.localStorage.setItem(key, JSON.stringify(valueToStore));
26 } catch (error) {
27 console.log(error);
28 }
29 };
30
31 return [storedValue, setValue];
32}
33
34// Usage
35function SpaceshipSettings() {
36 const [enginePower, setEnginePower] = useLocalStorage('enginePower', 50);
37
38 return (
39 <div>
40 <h2>Spaceship Settings</h2>
41 <label>Engine Power: {enginePower}%</label>
42 <input
43 type="range"
44 min="0"
45 max="100"
46 value={enginePower}
47 onChange={e => setEnginePower(Number(e.target.value))}
48 />
49 <button onClick={() => setEnginePower(0)}>Shut Down Engines</button>
50 </div>
51 );
52}A hook for handling HTTP requests:
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 abortController = new AbortController();
8 const signal = abortController.signal;
9
10 setLoading(true);
11
12 fetch(url, { signal })
13 .then(response => {
14 if (!response.ok) {
15 throw new Error('Network response was not ok');
16 }
17 return response.json();
18 })
19 .then(json => {
20 if (!signal.aborted) {
21 setData(json);
22 setError(null);
23 setLoading(false);
24 }
25 })
26 .catch(error => {
27 if (!signal.aborted) {
28 setError(error);
29 setData(null);
30 setLoading(false);
31 }
32 });
33
34 return () => {
35 abortController.abort();
36 };
37 }, [url]);
38
39 return { data, loading, error };
40}
41
42// Usage
43function AstronautList() {
44 const { data, loading, error } = useFetch('https://api.spacexdata.com/v4/crew');
45
46 if (loading) return <p>Loading crew...</p>;
47 if (error) return <p>Error: {error.message}</p>;
48
49 return (
50 <div>
51 <h2>SpaceX Astronauts</h2>
52 <ul>
53 {data.map(astronaut => (
54 <li key={astronaut.id}>
55 {astronaut.name} - {astronaut.agency}
56 </li>
57 ))}
58 </ul>
59 </div>
60 );
61}A simple hook for toggling boolean state:
1function useToggle(initialState = false) {
2 const [state, setState] = useState(initialState);
3
4 const toggle = useCallback(() => {
5 setState(prevState => !prevState);
6 }, []);
7
8 return [state, toggle];
9}
10
11// Usage
12function SpaceshipControl() {
13 const [shieldsActive, toggleShields] = useToggle(true);
14 const [enginesRunning, toggleEngines] = useToggle(false);
15
16 return (
17 <div className="control-panel">
18 <h2>Control Panel</h2>
19
20 <div className="control">
21 <span>Shields: {shieldsActive ? 'Active' : 'Inactive'}</span>
22 <button onClick={toggleShields}>
23 {shieldsActive ? 'Deactivate' : 'Activate'} shields
24 </button>
25 </div>
26
27 <div className="control">
28 <span>Engines: {enginesRunning ? 'Running' : 'Offline'}</span>
29 <button onClick={toggleEngines}>
30 {enginesRunning ? 'Shut Down' : 'Start'} engines
31 </button>
32 </div>
33 </div>
34 );
35}A hook for tracking window dimensions:
1function useWindowSize() {
2 const [windowSize, setWindowSize] = useState({
3 width: undefined,
4 height: undefined,
5 });
6
7 useEffect(() => {
8 // Handler to update state
9 function handleResize() {
10 setWindowSize({
11 width: window.innerWidth,
12 height: window.innerHeight,
13 });
14 }
15
16 // Add event listener
17 window.addEventListener('resize', handleResize);
18
19 // Call handler immediately to set initial size
20 handleResize();
21
22 // Remove event listener on unmount
23 return () => window.removeEventListener('resize', handleResize);
24 }, []); // Empty dependency array = run only once on mount
25
26 return windowSize;
27}
28
29// Usage
30function ResponsiveUI() {
31 const { width, height } = useWindowSize();
32
33 return (
34 <div>
35 <h2>Adaptive Interface</h2>
36 <p>Window width: {width}px</p>
37 <p>Window height: {height}px</p>
38
39 {width < 768 ? (
40 <MobileNavigation />
41 ) : (
42 <DesktopNavigation />
43 )}
44 </div>
45 );
46}A hook for delaying value updates, useful for example in search:
1function useDebounce(value, delay) {
2 const [debouncedValue, setDebouncedValue] = useState(value);
3
4 useEffect(() => {
5 // Set a timeout to update the value after the delay
6 const handler = setTimeout(() => {
7 setDebouncedValue(value);
8 }, delay);
9
10 // Cancel the previous timeout on each value change
11 return () => {
12 clearTimeout(handler);
13 };
14 }, [value, delay]);
15
16 return debouncedValue;
17}
18
19// Usage
20function SearchAstronauts() {
21 const [searchTerm, setSearchTerm] = useState('');
22 const debouncedSearchTerm = useDebounce(searchTerm, 500);
23 const [results, setResults] = useState([]);
24 const [isSearching, setIsSearching] = useState(false);
25
26 useEffect(() => {
27 if (debouncedSearchTerm) {
28 setIsSearching(true);
29 // Search for astronauts
30 fetchAstronauts(debouncedSearchTerm)
31 .then(data => {
32 setIsSearching(false);
33 setResults(data);
34 });
35 } else {
36 setResults([]);
37 }
38 }, [debouncedSearchTerm]);
39
40 return (
41 <div>
42 <input
43 type="text"
44 value={searchTerm}
45 onChange={e => setSearchTerm(e.target.value)}
46 />
47
48 {isSearching && <p>Searching...</p>}
49
50 <ul>
51 {results.map(result => (
52 <li key={result.id}>{result.name}</li>
53 ))}
54 </ul>
55 </div>
56 );
57}We can create more complex hooks by combining several simpler ones:
1function useSpaceMission(missionId) {
2 // Fetching mission data
3 const {
4 data: mission,
5 loading: missionLoading,
6 error: missionError
7 } = useFetch(`https://api.space.com/missions/${missionId}`);
8
9 // Fetching crew data
10 const {
11 data: crew,
12 loading: crewLoading,
13 error: crewError
14 } = useFetch(mission ? `https://api.space.com/missions/${missionId}/crew` : null);
15
16 // Fetching spacecraft data
17 const {
18 data: spacecraft,
19 loading: spacecraftLoading,
20 error: spacecraftError
21 } = useFetch(mission ? `https://api.space.com/spacecraft/${mission.spacecraftId}` : null);
22
23 // Tracking mission in localStorage
24 const [isFavorite, setIsFavorite] = useLocalStorage(`favorite-mission-${missionId}`, false);
25
26 // Function to toggle favorite mission status
27 const toggleFavorite = useCallback(() => {
28 setIsFavorite(prev => !prev);
29 }, [setIsFavorite]);
30
31 return {
32 mission,
33 crew,
34 spacecraft,
35 loading: missionLoading || crewLoading || spacecraftLoading,
36 error: missionError || crewError || spacecraftError,
37 isFavorite,
38 toggleFavorite
39 };
40}
41
42// Usage
43function MissionDetails({ missionId }) {
44 const {
45 mission,
46 crew,
47 spacecraft,
48 loading,
49 error,
50 isFavorite,
51 toggleFavorite
52 } = useSpaceMission(missionId);
53
54 if (loading) return <p>Loading mission details...</p>;
55 if (error) return <p>Error: Unable to load mission</p>;
56
57 return (
58 <div className="mission-details">
59 <h2>{mission.name}</h2>
60 <button onClick={toggleFavorite}>
61 {isFavorite ? 'Remove from favorites' : 'Add to favorites'}
62 </button>
63
64 <div className="mission-info">
65 <p>Launch date: {mission.launchDate}</p>
66 <p>Status: {mission.status}</p>
67 </div>
68
69 <div className="spacecraft-info">
70 <h3>Spacecraft: {spacecraft.name}</h3>
71 <p>Model: {spacecraft.model}</p>
72 </div>
73
74 <div className="crew-info">
75 <h3>Crew:</h3>
76 <ul>
77 {crew.map(member => (
78 <li key={member.id}>
79 {member.name} - {member.role}
80 </li>
81 ))}
82 </ul>
83 </div>
84 </div>
85 );
86}A good guideline for when to create a custom hook is the DRY (Don't Repeat Yourself) principle. If you notice that similar logic appears in many components, consider extracting it into a custom hook.
Typical signals that you need a custom hook:
Before hooks appeared in React 16.8, sharing logic between components was more difficult:
Custom hooks have several advantages over these patterns:
Custom hooks are a powerful abstraction mechanism in React. They allow you to:
By creating your own hooks, you become a more advanced React developer, capable of building elegant, modular, and maintainable applications.