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.
Before we start creating advanced hooks, we must know two fundamental rules, breaking which causes unpredictable errors:
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.
You cannot call hooks from regular JavaScript functions. Only:
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}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.
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}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}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.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] |Custom Hooks are the foundation of a well-organized React application:
use prefix - mandatory, allows React to verify rulesLike modular spaceship systems - each hook is a standalone module that can be installed, configured, and connected with other modules in any configuration.