JavaScript and React course Β· Module 5: useEffect and Custom Hooks

Creating Custom Hooks - Advanced Patterns

6 min read
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:

HookUse caseReturns
useToggleOn/off toggle{ value, toggle, setTrue, setFalse }
useCounterCounter with min/max{ count, increment, decrement, reset }
useFetchData fetching{ data, loading, error, refetch }
useDebounceDelaying valuesDelayed value
usePreviousPrevious valuePrevious value
useArrayArray operations{ array, push, remove, update, clear }
useLocalStoragePersistent state[value, setValue]

Summary

Custom Hooks are the foundation of a well-organized React application:

  1. Hook rules - always at the top level, only in components and hooks
  2. use prefix - mandatory, allows React to verify rules
  3. Composition - hooks can call other hooks, building complex behaviors
  4. Independent instances - each hook call has its own isolated state
  5. 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. 1. Why can you NOT call hooks inside conditional statements (if/else)?

  2. 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!

Useful articles