We use cookies to enhance your experience on the site
CodeWorlds

Creating custom hooks and popular custom hooks

During our cosmic journey through React, we have reached a point where the basic elements of our spaceship are well known to us. Now it is time to learn how to create our own specialized modules - custom hooks. These are what allow us to achieve a higher level of abstraction and reuse logic across components.

What are custom hooks?

Custom hooks are JavaScript functions that use built-in React hooks (like

useState
,
useEffect
,
useContext
, etc.) to encapsulate and share logic between components. This is one of the most powerful mechanisms in React, allowing you to:

  • Extract logic from components, making them simpler and more readable
  • Share logic between different components without code duplication
  • Better organize and test your code

Rules for creating custom hooks

Creating your own hooks follows a few simple rules:

  1. The name must start with "use" - e.g.
    useLocalStorage
    ,
    useFetch
    ,
    useSpaceshipStatus
  2. They can use other hooks - both built-in and other custom hooks
  3. They should return values - usually they return an array or object with values and functions
  4. They are regular JavaScript functions - there is no special syntax, they are just functions

Creating a simple custom hook

Let's start by creating a simple custom hook that manages form state:

1function useFormInput(initialValue) {
2  const [value, setValue] = useState(initialValue);
3  
4  function handleChange(e) {
5    setValue(e.target.value);
6  }
7  
8  return {
9    value,
10    onChange: handleChange
11  };
12}
13
14// Usage
15function LoginForm() {
16  const username = useFormInput('');
17  const password = useFormInput('');
18  
19  function handleSubmit(e) {
20    e.preventDefault();
21    console.log('Login:', username.value, 'Password:', password.value);
22  }
23  
24  return (
25    <form onSubmit={handleSubmit}>
26      <div>
27        <label>Username:</label>
28        <input type="text" {...username} />
29      </div>
30      <div>
31        <label>Password:</label>
32        <input type="password" {...password} />
33      </div>
34      <button type="submit">Login</button>
35    </form>
36  );
37}

Our

useFormInput
hook encapsulates the logic for storing and updating a form field value. It returns an object with the value and a change handler function, which we can spread directly as props on an input component.

Practical examples of popular custom hooks

Let's look at some commonly used custom hooks that can significantly simplify your work.

1. useLocalStorage

A hook for storing and synchronizing state with localStorage:

1function useLocalStorage(key, initialValue) {
2  // State to store our value
3  const [storedValue, setStoredValue] = useState(() => {
4    try {
5      // Get from localStorage by key
6      const item = window.localStorage.getItem(key);
7      // Parse stored json or return initialValue
8      return item ? JSON.parse(item) : initialValue;
9    } catch (error) {
10      // If an error occurs, return initialValue
11      console.log(error);
12      return initialValue;
13    }
14  });
15
16  // Return a function that saves to both useState and localStorage
17  const setValue = value => {
18    try {
19      // Allow value to be a function, just like useState
20      const valueToStore =
21        value instanceof Function ? value(storedValue) : value;
22      // Save to state
23      setStoredValue(valueToStore);
24      // Save to localStorage
25      window.localStorage.setItem(key, JSON.stringify(valueToStore));
26    } catch (error) {
27      console.log(error);
28    }
29  };
30
31  return [storedValue, setValue];
32}
33
34// Usage
35function SpaceshipSettings() {
36  const [enginePower, setEnginePower] = useLocalStorage('enginePower', 50);
37  
38  return (
39    <div>
40      <h2>Spaceship Settings</h2>
41      <label>Engine Power: {enginePower}%</label>
42      <input
43        type="range"
44        min="0"
45        max="100"
46        value={enginePower}
47        onChange={e => setEnginePower(Number(e.target.value))}
48      />
49      <button onClick={() => setEnginePower(0)}>Shut Down Engines</button>
50    </div>
51  );
52}

2. useFetch

A hook for handling HTTP requests:

1function useFetch(url) {
2  const [data, setData] = useState(null);
3  const [loading, setLoading] = useState(true);
4  const [error, setError] = useState(null);
5
6  useEffect(() => {
7    const abortController = new AbortController();
8    const signal = abortController.signal;
9    
10    setLoading(true);
11    
12    fetch(url, { signal })
13      .then(response => {
14        if (!response.ok) {
15          throw new Error('Network response was not ok');
16        }
17        return response.json();
18      })
19      .then(json => {
20        if (!signal.aborted) {
21          setData(json);
22          setError(null);
23          setLoading(false);
24        }
25      })
26      .catch(error => {
27        if (!signal.aborted) {
28          setError(error);
29          setData(null);
30          setLoading(false);
31        }
32      });
33      
34    return () => {
35      abortController.abort();
36    };
37  }, [url]);
38
39  return { data, loading, error };
40}
41
42// Usage
43function AstronautList() {
44  const { data, loading, error } = useFetch('https://api.spacexdata.com/v4/crew');
45  
46  if (loading) return <p>Loading crew...</p>;
47  if (error) return <p>Error: {error.message}</p>;
48  
49  return (
50    <div>
51      <h2>SpaceX Astronauts</h2>
52      <ul>
53        {data.map(astronaut => (
54          <li key={astronaut.id}>
55            {astronaut.name} - {astronaut.agency}
56          </li>
57        ))}
58      </ul>
59    </div>
60  );
61}

3. useToggle

A simple hook for toggling boolean state:

1function useToggle(initialState = false) {
2  const [state, setState] = useState(initialState);
3  
4  const toggle = useCallback(() => {
5    setState(prevState => !prevState);
6  }, []);
7  
8  return [state, toggle];
9}
10
11// Usage
12function SpaceshipControl() {
13  const [shieldsActive, toggleShields] = useToggle(true);
14  const [enginesRunning, toggleEngines] = useToggle(false);
15  
16  return (
17    <div className="control-panel">
18      <h2>Control Panel</h2>
19      
20      <div className="control">
21        <span>Shields: {shieldsActive ? 'Active' : 'Inactive'}</span>
22        <button onClick={toggleShields}>
23          {shieldsActive ? 'Deactivate' : 'Activate'} shields
24        </button>
25      </div>
26      
27      <div className="control">
28        <span>Engines: {enginesRunning ? 'Running' : 'Offline'}</span>
29        <button onClick={toggleEngines}>
30          {enginesRunning ? 'Shut Down' : 'Start'} engines
31        </button>
32      </div>
33    </div>
34  );
35}

4. useWindowSize

A hook for tracking window dimensions:

1function useWindowSize() {
2  const [windowSize, setWindowSize] = useState({
3    width: undefined,
4    height: undefined,
5  });
6  
7  useEffect(() => {
8    // Handler to update state
9    function handleResize() {
10      setWindowSize({
11        width: window.innerWidth,
12        height: window.innerHeight,
13      });
14    }
15    
16    // Add event listener
17    window.addEventListener('resize', handleResize);
18    
19    // Call handler immediately to set initial size
20    handleResize();
21    
22    // Remove event listener on unmount
23    return () => window.removeEventListener('resize', handleResize);
24  }, []); // Empty dependency array = run only once on mount
25  
26  return windowSize;
27}
28
29// Usage
30function ResponsiveUI() {
31  const { width, height } = useWindowSize();
32  
33  return (
34    <div>
35      <h2>Adaptive Interface</h2>
36      <p>Window width: {width}px</p>
37      <p>Window height: {height}px</p>
38      
39      {width < 768 ? (
40        <MobileNavigation />
41      ) : (
42        <DesktopNavigation />
43      )}
44    </div>
45  );
46}

5. useDebounce

A hook for delaying value updates, useful for example in search:

1function useDebounce(value, delay) {
2  const [debouncedValue, setDebouncedValue] = useState(value);
3  
4  useEffect(() => {
5    // Set a timeout to update the value after the delay
6    const handler = setTimeout(() => {
7      setDebouncedValue(value);
8    }, delay);
9    
10    // Cancel the previous timeout on each value change
11    return () => {
12      clearTimeout(handler);
13    };
14  }, [value, delay]);
15  
16  return debouncedValue;
17}
18
19// Usage
20function SearchAstronauts() {
21  const [searchTerm, setSearchTerm] = useState('');
22  const debouncedSearchTerm = useDebounce(searchTerm, 500);
23  const [results, setResults] = useState([]);
24  const [isSearching, setIsSearching] = useState(false);
25  
26  useEffect(() => {
27    if (debouncedSearchTerm) {
28      setIsSearching(true);
29      // Search for astronauts
30      fetchAstronauts(debouncedSearchTerm)
31        .then(data => {
32          setIsSearching(false);
33          setResults(data);
34        });
35    } else {
36      setResults([]);
37    }
38  }, [debouncedSearchTerm]);
39  
40  return (
41    <div>
42      <input
43        type="text"
44        value={searchTerm}
45        onChange={e => setSearchTerm(e.target.value)}
46      />
47      
48      {isSearching && <p>Searching...</p>}
49      
50      <ul>
51        {results.map(result => (
52          <li key={result.id}>{result.name}</li>
53        ))}
54      </ul>
55    </div>
56  );
57}

Combining multiple hooks - useSpaceMission

We can create more complex hooks by combining several simpler ones:

1function useSpaceMission(missionId) {
2  // Fetching mission data
3  const { 
4    data: mission, 
5    loading: missionLoading, 
6    error: missionError 
7  } = useFetch(`https://api.space.com/missions/${missionId}`);
8  
9  // Fetching crew data
10  const {
11    data: crew,
12    loading: crewLoading,
13    error: crewError
14  } = useFetch(mission ? `https://api.space.com/missions/${missionId}/crew` : null);
15  
16  // Fetching spacecraft data
17  const {
18    data: spacecraft,
19    loading: spacecraftLoading,
20    error: spacecraftError
21  } = useFetch(mission ? `https://api.space.com/spacecraft/${mission.spacecraftId}` : null);
22  
23  // Tracking mission in localStorage
24  const [isFavorite, setIsFavorite] = useLocalStorage(`favorite-mission-${missionId}`, false);
25  
26  // Function to toggle favorite mission status
27  const toggleFavorite = useCallback(() => {
28    setIsFavorite(prev => !prev);
29  }, [setIsFavorite]);
30  
31  return {
32    mission,
33    crew,
34    spacecraft,
35    loading: missionLoading || crewLoading || spacecraftLoading,
36    error: missionError || crewError || spacecraftError,
37    isFavorite,
38    toggleFavorite
39  };
40}
41
42// Usage
43function MissionDetails({ missionId }) {
44  const {
45    mission,
46    crew,
47    spacecraft,
48    loading,
49    error,
50    isFavorite,
51    toggleFavorite
52  } = useSpaceMission(missionId);
53  
54  if (loading) return <p>Loading mission details...</p>;
55  if (error) return <p>Error: Unable to load mission</p>;
56  
57  return (
58    <div className="mission-details">
59      <h2>{mission.name}</h2>
60      <button onClick={toggleFavorite}>
61        {isFavorite ? 'Remove from favorites' : 'Add to favorites'}
62      </button>
63      
64      <div className="mission-info">
65        <p>Launch date: {mission.launchDate}</p>
66        <p>Status: {mission.status}</p>
67      </div>
68      
69      <div className="spacecraft-info">
70        <h3>Spacecraft: {spacecraft.name}</h3>
71        <p>Model: {spacecraft.model}</p>
72      </div>
73      
74      <div className="crew-info">
75        <h3>Crew:</h3>
76        <ul>
77          {crew.map(member => (
78            <li key={member.id}>
79              {member.name} - {member.role}
80            </li>
81          ))}
82        </ul>
83      </div>
84    </div>
85  );
86}

Best practices for custom hooks

  1. Name hooks clearly - the name should indicate what the hook does
  2. Return appropriate values - you can return an array (like useState) or an object (more descriptive)
  3. Add documentation - document your hook's API (parameters, return values, example usage)
  4. Handle errors - anticipate potential problems and handle them appropriately
  5. Test hooks separately - custom hooks can (and should) be tested independently from components
  6. Always start with use - follow React conventions

When to create a custom hook?

A good guideline for when to create a custom hook is the DRY (Don't Repeat Yourself) principle. If you notice that similar logic appears in many components, consider extracting it into a custom hook.

Typical signals that you need a custom hook:

  • Repeating useEffect and useState logic
  • Complex logic obscuring the main functionality of a component
  • Need to share logic between components
  • Desire to increase testability of logic independently from components

Why are custom hooks better than other patterns?

Before hooks appeared in React 16.8, sharing logic between components was more difficult:

  1. Higher-Order Components (HOC) - wrapping components with other components
  2. Render props - passing rendering functions as props

Custom hooks have several advantages over these patterns:

  • Less code - less boilerplate code and nesting
  • Easier to follow - less "magic" and hidden dependencies
  • No additional components - they don't disrupt the component hierarchy
  • More natural API - using hooks is similar to using React's built-in hooks

Summary

Custom hooks are a powerful abstraction mechanism in React. They allow you to:

  1. Extract and share logic between components
  2. Create declarative, functional abstractions
  3. Improve component readability by separating logic from UI
  4. Increase code testability

By creating your own hooks, you become a more advanced React developer, capable of building elegant, modular, and maintainable applications.

Go to CodeWorlds