I always wrap React Query's useQuery
hook in a custom hook and never use it directly within a component. To see why, let's have a look at an example:
function TodoList() {
const { data: todos = [] } = useQuery(
['todos'],
() => fetchTodos()
);
return (
<ul>
{todos.map(todo => (
<li>{todo}</li>
)}
</ul>
);
}
The TodoList
component is quite simple: It fetches a list of to-dos from our backend and renders them within an unordered list. By using useQuery
directly, this component has to:
- specify a unique query key,
- be aware of the
fetchTodos()
function, - and provide a sensible initial value
[]
while the request hasn't completed yet.
The component is intended to render a to-do list, yet it is responsible for very technical decisions like these. In contrast, let's encapsulate the use of useQuery
in a custom hook:
function TodoList() {
const todos = useTodos();
return (
<ul>
{todos.map(todo => (
<li>{todo}</li>
)}
</ul>
);
}
function useTodos() {
const { data: todos = [] } = useQuery(
['todos'],
() => fetchTodos()
);
return todos;
}
By introducing a custom useTodos()
hook, we:
- provide a layer abstraction by separating what we want to do (get a list of to-dos) from how we do it (by using React Query),
- make the list of to-dos reusable throughout our application,
- make it possible to easily switch React Query for a different library later on,
- and improve the readability of our
TodoList
component.
The choice of a sensible query key, how a resource is fetched, and the configuration of useQuery
's options are implementation details that should always be hidden from components that only want to consume the resource managed by it.
I've used this pattern for quite a while, both in personal projects and at work. It has served me tremendously well. The pattern ties in with and is a concrete instance of Kyle Shevlin's great post useEncapsulation, which I can highly recommend for a more general view on this topic.
Top comments (0)