Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Custom hooks

A custom hook is an ordinary function whose name starts with use and which calls other hooks inside. It holds logic repeated across several components: loading data, subscribing to an event, working with localStorage. The component gets shorter, and the logic can be reused.

A data loading hook

In the blog, the post list, the post page and the comments all load data the same way: a request, loading, an error. Let us put that into a hook. Create the folder and the file src/hooks/useFetch.js:

// src/hooks/useFetch.js
import { useEffect, useState } from 'react';
 
export function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
 
  useEffect(() => {
    let ignore = false;
    setLoading(true);
    setError(null);
 
    fetch(url)
      .then((response) => {
        if (!response.ok) throw new Error(`Error ${response.status}`);
        return response.json();
      })
      .then((json) => {
        if (!ignore) setData(json);
      })
      .catch((err) => {
        if (!ignore) setError(err.message);
      })
      .finally(() => {
        if (!ignore) setLoading(false);
      });
 
    return () => {
      ignore = true;
    };
  }, [url]);
 
  return { data, loading, error };
}

The hook returns an object with three values. When url changes, the request is repeated.

Using it

The PostList component from the data loading lesson shrinks to a few lines:

// src/components/PostList.jsx
import { useFetch } from '../hooks/useFetch.js';
 
export default function PostList() {
  const { data: posts, loading, error } = useFetch(
    'https://jsonplaceholder.typicode.com/posts',
  );
 
  if (loading) return <p>Loading…</p>;
  if (error) return <p>Error: {error}</p>;
 
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

The page behaves as before: "Loading…", then the post list. The same hook works for a single post: useFetch('https://jsonplaceholder.typicode.com/posts/1').

Each call has its own state

A hook reuses logic, not data. If two components call useFetch, each gets its own data, loading and error and its own request.

A simple hook without an effect

A hook does not have to be big. A toggle for a menu or a spoiler — src/hooks/useToggle.js:

// src/hooks/useToggle.js
import { useState } from 'react';
 
export function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = () => setValue((current) => !current);
 
  return [value, toggle];
}

In a component: const [isOpen, toggle] = useToggle(); and a button with onClick={toggle}.

Rules of hooks

RuleWhy
Call hooks only at the top level of a component or another hookReact tells hooks apart by call order
Do not call hooks in conditions, loops or handlersThe order would differ between renders
A hook name starts with useThe linter finds hooks by it and checks the rules

Common mistake

A hook inside a condition breaks the call order: on one render it is there, on another it is not:

// Wrong:
function PostPage({ id }) {
  if (id) {
    const { data } = useFetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
  }
}

Always call the hook, and move the condition into the hook or the markup. The linter in a Vite project reports this with the react-hooks/rules-of-hooks rule.

Thanks for reading!

Donate to the site — we work hard for you.