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

Custom Hooks for Effects

5 min read
In this lesson6

On our cosmic journey through React, repeatedly writing the same useEffect patterns in different components is like building identical navigation systems from scratch for every ship in the fleet. Custom Hooks allow you to extract repeatable effect logic into a single, reusable function - like creating a propulsion module that can be installed in any ship.

What are Custom Hooks?

A Custom Hook is a regular JavaScript function whose name starts with use and which can call other hooks (useState, useEffect, useRef, etc.). It's not any special React syntax - the only convention is the use prefix.

1// Custom Hook - wyekstrahowana logika
2function useWindowSize() {
3  const [size, setSize] = useState({
4    width: window.innerWidth,
5    height: window.innerHeight
6  });
7
8  useEffect(() => {
9    const handleResize = () => {
10      setSize({ width: window.innerWidth, height: window.innerHeight });
11    };
12
13    window.addEventListener('resize', handleResize);
14    return () => window.removeEventListener('resize', handleResize);
15  }, []);
16
17  return size;
18}
19
20// Usage in a component - clean and simple
21function SpaceshipCockpit() {
22  const { width, height } = useWindowSize();
23
24  return (
25    <p>
26      Cockpit screen size: {width} x {height}
27    </p>
28  );
29}

Notice that useWindowSize contains useState and useEffect with cleanup inside - all the logic is enclosed in a single hook, and the component gets a ready result.

useFetch - data fetching

One of the most common custom hooks is a data fetching hook. Instead of repeating the loading/error/data pattern in every component, we encapsulate it in useFetch:

1function useFetch(url) {
2  const [data, setData] = useState(null);
3  const [isLoading, setIsLoading] = useState(true);
4  const [error, setError] = useState(null);
5
6  useEffect(() => {
7    const controller = new AbortController();
8    setIsLoading(true);
9    setError(null);
10
11    fetch(url, { signal: controller.signal })
12      .then(response => {
13        if (!response.ok) throw new Error('Network error');
14        return response.json();
15      })
16      .then(data => {
17        setData(data);
18        setIsLoading(false);
19      })
20      .catch(err => {
21        if (err.name !== 'AbortError') {
22          setError(err.message);
23          setIsLoading(false);
24        }
25      });
26
27    // Cleanup - cancel request on unmount or URL change
28    return () => controller.abort();
29  }, [url]);
30
31  return { data, isLoading, error };
32}

Now any component that needs API data can use a single line:

1function CrewList() {
2  const { data: crew, isLoading, error } = useFetch('/api/crew');
3
4  if (isLoading) return <p>Loading crew...</p>;
5  if (error) return <p>Error: {error}</p>;
6
7  return (
8    <ul>
9      {crew.map(member => (
10        <li key={member.id}>{member.name}</li>
11      ))}
12    </ul>
13  );
14}
15
16function MissionLog() {
17  const { data: missions, isLoading } = useFetch('/api/missions');
18
19  if (isLoading) return <p>Loading missions...</p>;
20
21  return missions.map(m => <div key={m.id}>{m.name}</div>);
22}

Two components, same data fetching pattern - zero code duplication.

useInterval - safe timer

A timer with setInterval requires cleanup and proper dependency handling. A custom hook encapsulates this logic:

1function useInterval(callback, delay) {
2  const savedCallback = useRef();
3
4  // Remember the latest callback version
5  useEffect(() => {
6    savedCallback.current = callback;
7  }, [callback]);
8
9  // Set timer
10  useEffect(() => {
11    if (delay === null) return; // null = pause timer
12
13    const tick = () => savedCallback.current();
14    const id = setInterval(tick, delay);
15    return () => clearInterval(id);
16  }, [delay]);
17}
18
19// Usage
20function MissionClock() {
21  const [seconds, setSeconds] = useState(0);
22  const [isRunning, setIsRunning] = useState(false);
23
24  useInterval(() => {
25    setSeconds(prev => prev + 1);
26  }, isRunning ? 1000 : null); // null = pauzuj
27
28  return (
29    <div>
30      <p>Mission time: {seconds}s</p>
31      <button onClick={() => setIsRunning(!isRunning)}>
32        {isRunning ? 'Pause' : 'Start'}
33      </button>
34    </div>
35  );
36}

Thanks to useRef for the callback, the timer always calls the latest version of the function without needing to restart the interval.

useLocalStorage - persistent state

A hook that synchronizes state with localStorage:

1function useLocalStorage(key, initialValue) {
2  const [storedValue, setStoredValue] = useState(() => {
3    try {
4      const item = window.localStorage.getItem(key);
5      return item ? JSON.parse(item) : initialValue;
6    } catch (error) {
7      return initialValue;
8    }
9  });
10
11  useEffect(() => {
12    try {
13      window.localStorage.setItem(key, JSON.stringify(storedValue));
14    } catch (error) {
15      console.error('Error writing to localStorage:', error);
16    }
17  }, [key, storedValue]);
18
19  return [storedValue, setStoredValue];
20}
21
22// Usage - works like useState, but persists data
23function ShipSettings() {
24  const [theme, setTheme] = useLocalStorage('theme', 'dark');
25  const [volume, setVolume] = useLocalStorage('volume', 50);
26
27  return (
28    <div>
29      <select value={theme} onChange={e => setTheme(e.target.value)}>
30        <option value="dark">Dark</option>
31        <option value="light">Light</option>
32      </select>
33      <input
34        type="range"
35        value={volume}
36        onChange={e => setVolume(Number(e.target.value))}
37      />
38    </div>
39  );
40}

After refreshing the page, settings are preserved - localStorage remembers values between sessions.

Rules for creating Custom Hooks

  1. Name starts with use - this is not optional. React requires this prefix to enforce hook rules
  2. Can call other hooks - useState, useEffect, useRef, and even other custom hooks
  3. Each call has its own state - two components using the same hook have independent copies of state
  4. Return what the component needs - can return values, arrays, objects, functions
1// Custom Hook with multiple return values
2function useToggle(initialValue = false) {
3  const [value, setValue] = useState(initialValue);
4
5  const toggle = () => setValue(prev => !prev);
6  const setTrue = () => setValue(true);
7  const setFalse = () => setValue(false);
8
9  return { value, toggle, setTrue, setFalse };
10}
11
12// Usage
13function ShieldControl() {
14  const shields = useToggle(false);
15
16  return (
17    <div>
18      <p>Shields: {shields.value ? 'ACTIVE' : 'DISABLED'}</p>
19      <button onClick={shields.toggle}>Toggle</button>
20      <button onClick={shields.setTrue}>Activate</button>
21      <button onClick={shields.setFalse}>Deactivate</button>
22    </div>
23  );
24}

Summary

Custom Hooks are one of the most powerful tools in React for code organization:

  1. Eliminate duplication - repeatable effect logic in one place
  2. Improve readability - components become shorter and more declarative
  3. Facilitate testing - hook logic can be tested independently from the component
  4. Are composable - custom hooks can call other custom hooks

Like modular spaceship systems - navigation system, communication system, defense system - each is a separate module (hook) that can be installed in any ship (component).

Code for this lesson: App.jsx
1import React, { useState, useEffect, useRef } from 'react';
2
3// Custom Hook: useToggle
4function useToggle(initialValue = false) {
5  const [value, setValue] = useState(initialValue);
6  const toggle = () => setValue(prev => !prev);
7  const setTrue = () => setValue(true);
8  const setFalse = () => setValue(false);
9  return { value, toggle, setTrue, setFalse };
10}
11
12// Custom Hook: useInterval
13function useInterval(callback, delay) {
14  const savedCallback = useRef();
15
16  useEffect(() => {
17    savedCallback.current = callback;
18  }, [callback]);
19
20  useEffect(() => {
21    if (delay === null) return;
22    const tick = () => savedCallback.current();
23    const id = setInterval(tick, delay);
24    return () => clearInterval(id);
25  }, [delay]);
26}
27
28// Custom Hook: useLocalStorage
29function useLocalStorage(key, initialValue) {
30  const [storedValue, setStoredValue] = useState(() => {
31    try {
32      const item = window.localStorage.getItem(key);
33      return item ? JSON.parse(item) : initialValue;
34    } catch {
35      return initialValue;
36    }
37  });
38
39  useEffect(() => {
40    try {
41      window.localStorage.setItem(key, JSON.stringify(storedValue));
42    } catch (error) {
43      console.error('Save error:', error);
44    }
45  }, [key, storedValue]);
46
47  return [storedValue, setStoredValue];
48}
49
50// Demo 1: useToggle
51function ShieldControls() {
52  const shields = useToggle(false);
53  const weapons = useToggle(false);
54  const engines = useToggle(true);
55
56  return (
57    <div className="demo-card">
58      <h2>useToggle</h2>
59      <div className="system-row">
60        <span>Shields</span>
61        <span className={shields.value ? 'on' : 'off'}>{shields.value ? 'ON' : 'OFF'}</span>
62        <button onClick={shields.toggle}>Toggle</button>
63      </div>
64      <div className="system-row">
65        <span>Weapons</span>
66        <span className={weapons.value ? 'on' : 'off'}>{weapons.value ? 'ON' : 'OFF'}</span>
67        <button onClick={weapons.toggle}>Toggle</button>
68      </div>
69      <div className="system-row">
70        <span>Engines</span>
71        <span className={engines.value ? 'on' : 'off'}>{engines.value ? 'ON' : 'OFF'}</span>
72        <button onClick={engines.toggle}>Toggle</button>
73      </div>
74    </div>
75  );
76}
77
78// Demo 2: useInterval
79function MissionClock() {
80  const [seconds, setSeconds] = useState(0);
81  const isRunning = useToggle(false);
82
83  useInterval(() => {
84    setSeconds(prev => prev + 1);
85  }, isRunning.value ? 1000 : null);
86
87  const mins = String(Math.floor(seconds / 60)).padStart(2, '0');
88  const secs = String(seconds % 60).padStart(2, '0');
89
90  return (
91    <div className="demo-card">
92      <h2>useInterval</h2>
93      <div className="clock">{mins}:{secs}</div>
94      <div className="clock-controls">
95        <button onClick={isRunning.toggle}>
96          {isRunning.value ? 'Pause' : 'Start'}
97        </button>
98        <button onClick={() => { isRunning.setFalse(); setSeconds(0); }}>
99          Reset
100        </button>
101      </div>
102    </div>
103  );
104}
105
106// Demo 3: useLocalStorage
107function ShipSettings() {
108  const [pilotName, setPilotName] = useLocalStorage('pilotName', '');
109  const [theme, setTheme] = useLocalStorage('theme', 'dark');
110  const [volume, setVolume] = useLocalStorage('volume', 50);
111
112  return (
113    <div className="demo-card">
114      <h2>useLocalStorage</h2>
115      <p className="hint">Data survives a page refresh!</p>
116      <div className="setting">
117        <label>Pilot:</label>
118        <input
119          value={pilotName}
120          onChange={e => setPilotName(e.target.value)}
121          placeholder="Enter name..."
122        />
123      </div>
124      <div className="setting">
125        <label>Theme:</label>
126        <select value={theme} onChange={e => setTheme(e.target.value)}>
127          <option value="dark">Dark</option>
128          <option value="light">Light</option>
129          <option value="blue">Cosmic</option>
130        </select>
131      </div>
132      <div className="setting">
133        <label>Volume: {volume}%</label>
134        <input
135          type="range"
136          min="0"
137          max="100"
138          value={volume}
139          onChange={e => setVolume(Number(e.target.value))}
140        />
141      </div>
142    </div>
143  );
144}
145
146function App() {
147  return (
148    <div className="app">
149      <h1>Custom Hooks - Modular ship systems</h1>
150      <div className="demos">
151        <ShieldControls />
152        <MissionClock />
153        <ShipSettings />
154      </div>
155    </div>
156  );
157}
158
159export default App;

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. Which of the following is NOT a typical use case for useEffect?

  2. 2. Which React hook replaces the componentDidMount method from class components?

Hands-on tasks in the game

  • Code editor

    Custom Media Query Hook

  • Code editor

    Battery Status Monitoring

Useful articles