Last week, one of my apprentices shared an article with me called useEncapsulation. In it, Kyle Shevlin argues that React components should only use custom hooks. This is because they make it easier to follow what your code actually does without getting lost in its implementation. It’s an interesting read, but my apprentice wanted me to explain it a little further. Why use custom hooks?

Custom hooks are useful for sharing stateful logic between components. They are also useful for improving readability, which is the point Kyle makes in his article. To help us understand custom hooks, we’ll create one for fetching data, although it will be very basic and only for learning purposes. There are better ways to fetch data in React that account for the hard parts: deduplicating requests, caching responses, and avoiding network waterfalls. These things are difficult to do well.

Let’s say we want to render a list of posts from JSONPlaceholder. We’ll use three pieces of state: posts, error, and isLoading. In our Effect, we’ll fetch the API data and update the state. Then we’ll conditionally render the UI depending on the state.

function App() {
const [posts, setPosts] = useState(null);
const [error, setError] = useState(false);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
const API = "https://jsonplaceholder.typicode.com/posts";

async function getPosts() {
try {
const response = await fetch(API);
if (!response.ok) throw Error();

const posts = await response.json();
setPosts(posts);
} catch {
setError(true);
} finally {
setIsLoading(false);
}
}

getPosts();
}, []);

if (error) return <p>Failed to fetch posts!</p>;
if (isLoading) return <p>Fetching posts...</p>;

return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}

At a glance, it’s hard to tell what this Effect actually does. The component is 36 lines long, 19 of which (over half) are just for the Effect. Even if we don’t end up reusing it, wouldn’t it be nice to move this stateful logic into a custom hook called useFetch()? All we’d be doing is moving a chunk of code into a named function—something we do all the time to improve readability. The only difference is that hooks have a few rules.

function useFetch(url) {
const [data, setData] = useState(null);
const [error, setError] = useState(false);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
async function getData() {
try {
const response = await fetch(url);
if (!response.ok) throw Error();

const data = await response.json();
setData(data);
} catch {
setError(true);
} finally {
setIsLoading(false);
}
}

getData();
}, [url]);

return { data, error, isLoading };
}

Using our custom hook, we can make our component much cleaner. More importantly, we can focus on what the custom hook does—fetch data from an API—without worrying about how it does it. This is what Kyle advocates for in his article.

function App() {
const API = "https://jsonplaceholder.typicode.com/posts";
const { data: posts, error, isLoading } = useFetch(API);

if (error) return <p>Failed to fetch posts!</p>;
if (isLoading) return <p>Fetching posts...</p>;

return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}

View demo on StackBlitz.

The goal of this article is to offer a simple example that helps you understand why you might use a custom hook. For a more detailed introduction, check out Reusing Logic with Custom Hooks in the React docs. It’s really helpful!