We use cookies to enhance your experience on the site
CodeWorlds

Custom Hooks for Effects

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).

Go to CodeWorlds