JavaScript and React course Β· Module 5: useEffect and Custom Hooks
Creating Custom Hooks - Advanced Patterns
In this lesson7
In the React cosmic fleet, one of the most valuable engineering skills is designing reusable modules. Imagine building navigation, communication, and diagnostic systems for hundreds of spaceships. Instead of writing each system from scratch for every ship, you create universal modules that can be connected to any vehicle. That's exactly how Custom Hooks work in React.
In the previous lesson, we learned the basics of custom hooks - useFetch, useInterval, useLocalStorage, and useToggle. Now we'll dive into advanced patterns, hook rules, and composition that will make your hooks truly professional.
Rules of Hooks
Before we start creating advanced hooks, we must know two fundamental rules, breaking which causes unpredictable errors:
Rule 1: Only call hooks at the top level
Hooks must always be called in the same order on every render. Never place hooks inside conditions, loops, or nested functions:
1// BAD - hook inside condition
2function SpaceShip({ hasRadar }) {
3 if (hasRadar) {
4 const [signal, setSignal] = useState(null); // ERROR!
5 }
6}
7
8// GOOD - warunek wewnatrz hooka
9function SpaceShip({ hasRadar }) {
10 const [signal, setSignal] = useState(null);
11
12 useEffect(() => {
13 if (hasRadar) {
14 // radar logic
15 }
16 }, [hasRadar]);
17}React tracks hooks based on their call order. If a condition causes one hook to be skipped, all subsequent hooks will have incorrect values - as if all the modules in the spaceship shifted positions.
Rule 2: Only call hooks from React components or custom hooks
You cannot call hooks from regular JavaScript functions. Only:
- React functional components
- Custom Hooks (functions starting with
use)
1// BAD - regular function
2function calculateTrajectory() {
3 const [speed] = useState(0); // ERROR!
4}
5
6// GOOD - custom hook
7function useTrajectory() {
8 const [speed, setSpeed] = useState(0);
9 return { speed, setSpeed };
10}useDebounce - delaying actions
The useDebounce hook is extremely useful for real-time searching. Instead of sending an API request after every keystroke, we wait until the user finishes typing:
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 - wyszukiwanie planet
16function PlanetSearch() {
17 const [query, setQuery] = useState('');
18 const debouncedQuery = useDebounce(query, 500);
19
20 useEffect(() => {
21 if (debouncedQuery) {
22 // Request sent only after 500ms pause in typing
23 fetch(`/api/planets?search=${debouncedQuery}`);
24 }
25 }, [debouncedQuery]);
26
27 return (
28 <input
29 value={query}
30 onChange={e => setQuery(e.target.value)}
31 placeholder="Search planets..."
32 />
33 );
34}The timer is cleared on every value change. Only when the user stops typing for 500ms will the value be updated and the API request sent.
usePrevious - remembering the previous value
This simple hook allows comparing the current value with the previous one:
1function usePrevious(value) {
2 const ref = useRef();
3
4 useEffect(() => {
5 ref.current = value;
6 }, [value]);
7
8 return ref.current;
9}
10
11// Usage - monitoring speed changes
12function SpeedMonitor() {
13 const [speed, setSpeed] = useState(0);
14 const previousSpeed = usePrevious(speed);
15
16 const trend = speed > previousSpeed
17 ? 'Accelerating'
18 : speed < previousSpeed
19 ? 'Decelerating'
20 : 'Constant speed';
21
22 return <p>{trend}: {speed} km/s (was: {previousSpeed})</p>;
23}useArray - managing arrays
Arrays require specific operations (adding, removing, filtering). A custom hook encapsulates these operations in a clean interface:
1function useArray(initialArray = []) {
2 const [array, setArray] = useState(initialArray);
3
4 const push = (element) => setArray(a => [...a, element]);
5 const removeByIndex = (index) =>
6 setArray(a => a.filter((_, i) => i !== index));
7 const update = (index, newElement) =>
8 setArray(a => a.map((item, i) => (i === index ? newElement : item)));
9 const clear = () => setArray([]);
10 const filter = (fn) => setArray(a => a.filter(fn));
11
12 return { array, set: setArray, push, removeByIndex, update, clear, filter };
13}
14
15// Usage - crew list
16function CrewManager() {
17 const crew = useArray(['Captain Nova', 'Navigator Rex']);
18
19 return (
20 <div>
21 {crew.array.map((member, i) => (
22 <div key={i}>
23 {member}
24 <button onClick={() => crew.removeByIndex(i)}>Remove</button>
25 </div>
26 ))}
27 <button onClick={() => crew.push('New member')}>
28 Add crew member
29 </button>
30 <button onClick={crew.clear}>Dismiss crew</button>
31 </div>
32 );
33}Hook composition - combining modules
The true power of custom hooks reveals itself when we combine them with each other. Hooks can call other hooks, building complex behaviors from simple blocks:
1// Hook 1: Simple state hook with validation
2function useValidatedInput(initialValue, validator) {
3 const [value, setValue] = useState(initialValue);
4 const [error, setError] = useState(null);
5
6 const handleChange = (newValue) => {
7 setValue(newValue);
8 const validationError = validator(newValue);
9 setError(validationError);
10 };
11
12 return { value, error, onChange: handleChange, isValid: !error };
13}
14
15// Hook 2: Composition - form with multiple fields
16function useSpaceshipForm() {
17 const name = useValidatedInput('', (v) =>
18 v.length < 3 ? 'Min. 3 characters' : null
19 );
20 const fuel = useValidatedInput(100, (v) =>
21 v < 0 || v > 100 ? 'Range 0-100' : null
22 );
23
24 const isFormValid = name.isValid && fuel.isValid;
25
26 const reset = () => {
27 name.onChange('');
28 fuel.onChange(100);
29 };
30
31 return { name, fuel, isFormValid, reset };
32}
33
34// Usage
35function ShipRegistration() {
36 const form = useSpaceshipForm();
37
38 return (
39 <form>
40 <input
41 value={form.name.value}
42 onChange={e => form.name.onChange(e.target.value)}
43 />
44 {form.name.error && <span>{form.name.error}</span>}
45
46 <input
47 type="number"
48 value={form.fuel.value}
49 onChange={e => form.fuel.onChange(Number(e.target.value))}
50 />
51 {form.fuel.error && <span>{form.fuel.error}</span>}
52
53 <button disabled={!form.isFormValid}>Register ship</button>
54 </form>
55 );
56}useSpaceshipForm uses useValidatedInput twice - each call creates an independent instance with its own state.
Pattern: useToggle, useCounter, useForm
Popular custom hook patterns worth knowing:
| Hook | Use case | Returns |
|---|---|---|
useToggle | On/off toggle | { value, toggle, setTrue, setFalse } |
useCounter | Counter with min/max | { count, increment, decrement, reset } |
useFetch | Data fetching | { data, loading, error, refetch } |
useDebounce | Delaying values | Delayed value |
usePrevious | Previous value | Previous value |
useArray | Array operations | { array, push, remove, update, clear } |
useLocalStorage | Persistent state | [value, setValue] |
Summary
Custom Hooks are the foundation of a well-organized React application:
- Hook rules - always at the top level, only in components and hooks
useprefix - mandatory, allows React to verify rules- Composition - hooks can call other hooks, building complex behaviors
- Independent instances - each hook call has its own isolated state
- Reusability - one logic, many components
Like modular spaceship systems - each hook is a standalone module that can be installed, configured, and connected with other modules in any configuration.
Code for this lesson: App.jsx
1import React, { useState, useEffect, useRef } from 'react';
2
3// === useDebounce ===
4function useDebounce(value, delay) {
5 const [debouncedValue, setDebouncedValue] = useState(value);
6
7 useEffect(() => {
8 const timer = setTimeout(() => setDebouncedValue(value), delay);
9 return () => clearTimeout(timer);
10 }, [value, delay]);
11
12 return debouncedValue;
13}
14
15// === usePrevious ===
16function usePrevious(value) {
17 const ref = useRef();
18 useEffect(() => { ref.current = value; }, [value]);
19 return ref.current;
20}
21
22// === useToggle ===
23function useToggle(initial = false) {
24 const [value, setValue] = useState(initial);
25 const toggle = () => setValue(v => !v);
26 const setTrue = () => setValue(true);
27 const setFalse = () => setValue(false);
28 return { value, toggle, setTrue, setFalse };
29}
30
31// === useArray ===
32function useArray(initialArray = []) {
33 const [array, setArray] = useState(initialArray);
34 const push = (el) => setArray(a => [...a, el]);
35 const removeByIndex = (i) => setArray(a => a.filter((_, idx) => idx !== i));
36 const clear = () => setArray([]);
37 return { array, push, removeByIndex, clear, set: setArray };
38}
39
40// === useLocalStorage ===
41function useLocalStorage(key, initialValue) {
42 const [storedValue, setStoredValue] = useState(() => {
43 try {
44 const item = window.localStorage.getItem(key);
45 return item ? JSON.parse(item) : initialValue;
46 } catch { return initialValue; }
47 });
48
49 useEffect(() => {
50 try { window.localStorage.setItem(key, JSON.stringify(storedValue)); }
51 catch (e) { console.error(e); }
52 }, [key, storedValue]);
53
54 return [storedValue, setStoredValue];
55}
56
57// --- Demo Components ---
58
59function DebounceDemo() {
60 const [text, setText] = useState('');
61 const debouncedText = useDebounce(text, 600);
62 const [results, setResults] = useState([]);
63
64 useEffect(() => {
65 if (debouncedText) {
66 const planets = ['Mercury','Venus','Earth','Mars','Jupiter','Saturn','Uranus','Neptune'];
67 setResults(planets.filter(p =>
68 p.toLowerCase().includes(debouncedText.toLowerCase())
69 ));
70 } else {
71 setResults([]);
72 }
73 }, [debouncedText]);
74
75 return (
76 <div className="demo-card">
77 <h3>useDebounce</h3>
78 <input value={text} onChange={e => setText(e.target.value)} placeholder="Search for planets..." />
79 <p className="hint">Debounced: "{debouncedText}"</p>
80 {results.length > 0 && (
81 <ul className="results">{results.map(r => <li key={r}>{r}</li>)}</ul>
82 )}
83 </div>
84 );
85}
86
87function PreviousDemo() {
88 const [speed, setSpeed] = useState(0);
89 const prevSpeed = usePrevious(speed);
90
91 const trend = speed > (prevSpeed || 0) ? 'Accelerating' :
92 speed < (prevSpeed || 0) ? 'Braking' : 'Constant speed';
93
94 return (
95 <div className="demo-card">
96 <h3>usePrevious</h3>
97 <p className="speed-display">{speed} km/s</p>
98 <p className="hint">Previously: {prevSpeed ?? 0} km/s | {trend}</p>
99 <div className="btn-row">
100 <button onClick={() => setSpeed(s => s + 10)}>+10</button>
101 <button onClick={() => setSpeed(s => Math.max(0, s - 10))}>-10</button>
102 <button onClick={() => setSpeed(0)}>Reset</button>
103 </div>
104 </div>
105 );
106}
107
108function ToggleDemo() {
109 const shields = useToggle(false);
110 const engines = useToggle(true);
111
112 return (
113 <div className="demo-card">
114 <h3>useToggle</h3>
115 <div className="toggle-item">
116 <span>Shields: <b style={{color: shields.value ? '#2ecc71' : '#e74c3c'}}>
117 {shields.value ? 'ACTIVE' : 'DISABLED'}
118 </b></span>
119 <button onClick={shields.toggle}>Toggle</button>
120 </div>
121 <div className="toggle-item">
122 <span>Engines: <b style={{color: engines.value ? '#2ecc71' : '#e74c3c'}}>
123 {engines.value ? 'ACTIVE' : 'DISABLED'}
124 </b></span>
125 <button onClick={engines.toggle}>Toggle</button>
126 </div>
127 </div>
128 );
129}
130
131function ArrayDemo() {
132 const crew = useArray(['Captain Nova', 'Navigator Rex']);
133 const [name, setName] = useState('');
134
135 const addMember = () => {
136 if (name.trim()) { crew.push(name.trim()); setName(''); }
137 };
138
139 return (
140 <div className="demo-card">
141 <h3>useArray</h3>
142 <div className="crew-list">
143 {crew.array.map((member, i) => (
144 <div key={i} className="crew-member">
145 <span>{member}</span>
146 <button onClick={() => crew.removeByIndex(i)} className="btn-small">X</button>
147 </div>
148 ))}
149 </div>
150 <div className="add-row">
151 <input value={name} onChange={e => setName(e.target.value)}
152 placeholder="Crew member name..." onKeyDown={e => e.key === 'Enter' && addMember()} />
153 <button onClick={addMember}>Add</button>
154 </div>
155 <button onClick={crew.clear} className="btn-danger">Release the crew</button>
156 </div>
157 );
158}
159
160function StorageDemo() {
161 const [theme, setTheme] = useLocalStorage('space-theme', 'dark');
162 const [volume, setVolume] = useLocalStorage('space-volume', 50);
163
164 return (
165 <div className="demo-card">
166 <h3>useLocalStorage</h3>
167 <div className="setting">
168 <label>Theme:</label>
169 <select value={theme} onChange={e => setTheme(e.target.value)}>
170 <option value="dark">Dark</option>
171 <option value="light">Light</option>
172 <option value="cosmic">Cosmic</option>
173 </select>
174 </div>
175 <div className="setting">
176 <label>Volume: {volume}%</label>
177 <input type="range" min="0" max="100" value={volume}
178 onChange={e => setVolume(Number(e.target.value))} />
179 </div>
180 <p className="hint">Values saved in localStorage</p>
181 </div>
182 );
183}
184
185export default function App() {
186 return (
187 <div className="app">
188 <h1>Custom Hooks - Spaceship modules</h1>
189 <div className="grid">
190 <DebounceDemo />
191 <PreviousDemo />
192 <ToggleDemo />
193 <ArrayDemo />
194 <StorageDemo />
195 </div>
196 </div>
197 );
198}Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Why can you NOT call hooks inside conditional statements (if/else)?
2. How does a custom hook differ from a regular JavaScript function?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Create a custom hook useDebounce(value, delay) returning a debounced value and useToggle(initialValue) returning { value, toggle, setTrue, setFalse }. Both hooks should work with the test components on the page.
- Vertical ordering
Order the steps for creating a custom hook from start to finish:
- Code editor
Create a custom hook useArray(initialArray) returning { array, set, push, removeByIndex, update, clear, filter } and usePrevious(value) returning the previous value. The hooks should work with the crew management component on the page.
- Click in order
Arrange the useDebounce custom hook syntax in the correct order:
- Code editor
Build a comprehensive Saturn Ring Observatory Control Panel containing: useEffect for monitoring various ring orbital phases, useLayoutEffect for real-time measurement synchronization, cleanup functions for proper sensor unmounting, dependency arrays for precise re-run control, multiple effects for independent monitoring systems (RingA, RingB, RingC monitoring), effect optimization patterns, async effects for data fetching from space probes, and conditional effects responding to different observatory states. Create a monitoring system worthy of the most advanced Saturn ring observatory!