Have you ever come onto a project and you notice there are multiple ways to call apis and you just get confused to which to use? Some ways might be overly complicated for what you need to do, you don't know which one to use and then when you get to a code review you get told to use the other way?
I wanted to find a way we could have a standardised way of calling apis so that the UI was kept simple and had information to use for the UI to display different states without having all that logic built in every time you needed to call an api. You normally get something like this…
import { useState } from 'react';
const Component = () => {
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState();
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetch = async () => {
try {
setIsLoading(true);
const response = await fetch('/api/users');
if (response.status === 200) {
const data = await response.json();
setIsSuccess(true);
setData(data);
setIsLoading(false);
} else {
setIsError(true);
}
} catch(e) {
setIsError(true);
}
}
}, []);
isLoading && return <p>IsLoading</p>;
isError && return <p>Error</p>;
return <p>{data ? JSON.stringify(data) : 'Empty'}</p>
}
Overtime, this looks a bit mental. Nothing caters for caching here, it fetches every time the component mounts and it is not reusable in another component.
In the project I was working on we was using redux anyway so I thought how could we use RTK query to make our lives easier.
🤔 What is RTK Query?
RTK Query is a tool you integrate with redux toolkit that makes handling data in your web apps a whole lot easier. It helps you fetch data from a server, manage it in your app, and keeps it up to date without all the usual headaches.
01. 📦 Creating apis
RTK query only takes minutes to setup before you can start setting up API endpoints.
📝 Example: This is our example of use setting up a basic set of APIS which get the user which share an authorisation token.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers) => {
// Example if you need to apply a header on all apis
headers.set('Authorization', 'Bearer TOKEN');
return headers;
},
}),
endpoints: (builder) => ({
getUsers: builder.query({
query: () => 'users' // /api/users
}),
getUser: builder.query({
query: (id) => `users/${id}`
}),
updateUser: builder.mutation({
query: (id, body) => `users/${id}`,
method: 'PATCH',
body,
}),
deleteUser: builder.mutation({
query: (id) => `users/${id}`,
method: 'DELETE',
})
})
});
👍 Good for: Having a standardised way for setting up endpoints and sharing headers between them.
02.🪝 React Hooks
No more creating your own custom hooks for apis! RTK creates react hooks for you when you add a new endpoint.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getUsers: builder.query({
query: () => 'users' // /api/users
}),
})
});
export const {
useGetUsersQuery,
} = api;
How awesome is that? That would have been about a solid day or two creating a fancy react hook that tries to solve all purposes that is now resolved as soon as you make a new endpoint.
Queries
As soon as the component mounts we fetch off to the api to get the data we need.
📝 Example: Fetching all users showing different visual states.
import { useGetUsersQuery } from '../apis';
const Component = () => {
const {
data, // the response from the api
isError, // has an error
isLoading, // first mount loading
isFetching, // is Fetching again after the first mount
isSuccess, // is 200
error // Error message
} = useGetUserQuery();
// Purely for demo purposes
return <p>{JSON.stringify(data)}</p>
}
👍 Good for: Shared pattern across the app making it easier to understand.
Mutations
Mutations allow you to make a change to an endpoint based on an action that has been fired in the app.
📝 Example: Deleting a user when clicking a button.
import { useState } from 'react';
import { useDeleteUserMutation } from '../apis';
const Component = () => {
const [
deleteUserPromise,
{ isSuccess, isError, data }
] = useDeleteUserMutation();
const handleClick = async () => {
await deleteUserPromise('userOne');
}
return (
<button type="button" onClick={handleClick}>
Delete user
</button>
);
}
👍 Good for: Shared pattern across the app making it easier to understand.
Lazy Queries
Lazy queries behave like mutations but are used more for get requests that you need to do after an action is called.
📝 Example: Getting a user by id when you click a button.
import { useState } from 'react';
import { useLazyGetUserQuery } from '../apis';
const Component = () => {
const [user, setUser] = useState();
const [
getUserPromise,
{} // Same data as a query
] = useLazyGetUserQuery();
const handleClick = async () => {
const { data } = await getUserPromise('userOne');
if (data) {
setUser(data);
}
}
return (
<>
<button type="button" onClick={handleClick}>
Get user
</button>
{user && <p>{user.userName}</p>}
</>
);
}
👍 Good for: When you need get data from an action instead of on mount of a component.
👀 Watch out …
Queries/Lazy Queries are cached endpoints by default so when you change some code with a mutation you might get a cached response from the queries ….
03. ⚙️ Let's fix that
Default cache times
The default cache time for a query is 60 seconds.
When multiple components use the same hook, it will prevent the same api from being called multiple times which reduces overhead on your server 💰. When the first one is resolved, it is resolved for everywhere that calls it.
📝 Example: Setting getUser to have 0 second cache.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getUser: builder.query({
query: (id) => `users/${id}`,
keepUnusedDataFor: 0,
}),
})
});
🕵️Tip: keepUnusedDataFor is good for apis that you need to get fresh data from every time. This also can be inherited when set at the top level api.
Invalidating caches with Tags
Sometimes you just need to update the cache when you call a mutation. This is where RTK query provides you with Tags.
📝 Example: Using tagTypes to define what tags I want to define. When deleteUser gets called it will invalidate anything with provideTags: ["Users"].
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Users'],
endpoints: (builder) => ({
getUsers: builder.query({
query: () => 'users',
provideTags: ['Users']
}),
deleteUser: builder.mutation({
query: (id) => `users/${id}`,
method: 'DELETE',
invalidatesTags: ['Users']
})
})
});
Refetching
Now we have tags in place, we can use a method that returns back from the query called "refetch" which will get us the latest set of users when the deleteUser method has completed.
📝 Example: When deleteUser is fired, it will invalidate the getUsers cache and then "refetch" will get a new set of users.
import { useEffect } from 'react';
import { useGetUsersQuery, useDeleteUserMutation } from '../apis';
const Component = () => {
const { data, refetch } = useGetUserQuery(user);
const [deleteUser, { isSuccess }] = useDeleteUserMutation();
const handleClick = async () => {
await deleteUser('userTwo');
}
useEffect(() => {
if (isSuccess) {
// even though the tag is invalidated, there
// is nothing telling useGetUser to refetch the
// the new cache
refetch();
}
}, [isSuccess]);
return (
<button type="button" onClick={handleClick}>
Update user
</button>
);
}
04. 🔄 Other Features
Polling
RTK Query can take secondary params to change how often to re-fetch the data.
📝 Example: Polling for changes to our get all users.
import { useGetUsersQuery } from '../apis';
const Component = () => {
const { data } = useGetUserQuery(undefined, { polling: 3000 });
// Purely for demo purposes
return <p>{JSON.stringify(data)}</p>
}
🟠Be careful: Use this pragmatically as it could become costly on the server side.
Automatic Refetching
When a query takes params to fetch specific data, it will cache that specific request for 60 seconds. By making the query dynamic with query parameters, the data is passed into the hook updates the state returned as it calls a new endpoint path.
📝 Example: Updating the user id when pressing the button which fetches the other user.
import { useState } from 'react';
import { useGetUsersQuery } from '../apis';
const Component = () => {
const [user, setUser] = useState('userOne');
const { data } = useGetUserQuery(user);
const handleClick = () => {
setUser('userTwo');
}
return (
<button type="button" onClick={handleClick}>
Update user
</button>
);
}
05. ⛓️ Dependency Management
RTK Query can take secondary parameter called skip which will tell RTK to not call this api until the id is defined.
📝 Example: Using skip parameter to prevent the query from being called.
import { useGetUsersQuery } from '../apis';
const Component = () => {
const [user, setUser] = useState(undefined);
const { data } = useGetUserQuery(user, { skip: user === undefined });
const handleClick = () => {
setUser('userTwo');
}
return (
<button type="button" onClick={handleClick}>
Update user
</button>
);
}
💣 What doesn't do well?
If you are like me who is a NextJS fanboy then you will have noticed that in version 13 there is a big buzz around server components and moving all JS code back to the server so we render more on the sever which makes the client faster. RTK is a client-side library so this does not work with server components.
🏁 Conclusion
In summary, the main reason why I like using this tool is purely around DevEx. I like how multiple teams can easily adopt this library and then when you move around teams you can easily understand how these apis work as there is a standardised pattern of implementing endpoints across the app.
Not only that the DevEx is good, it also makes my unit tests a lot nicer as I am mocking the variables that come from the react hooks which makes my confidence in my tests a lot higher as I can test more of the UI.
This is not saying use RTK for everything, as there are many libraries that do this however if you are using toolkit on your project maybe have a look at this?
What are your experiences using it?
Top comments (2)
Awesome post, Matt ✨ - I'm integrating it at the moment on a project.
I think
prepareHeaders
goes in thefetchBaseQuery
args if I remember correctly?Good spot mate!