
Article written by

Raphaël Huiban
Developer JavaScript
What is React Query and how is it implemented at Neomanis?
At Neomanis, we started developing the front-end of the application without using an external library to manage our various API requests. This worked well at the beginning, because the application's functionality remained simple.
For example, here is what a simple component containing a query might look like:
function UserList() {
const [users, setUsers] = useState<User[]>();
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchData = async () => {
try {
const data = await getUsers();
setUsers(data);
setIsLoading(false);
} catch (error) {
setIsError(true);
}
};
fetchData();
}, []);
if (isLoading) {
return <Loading />;
}
if (isError) {
return <Error />;
}
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
This is a very simple use case, but it still requires three states to manage the data, the loader and the error. Often, we can add the fact that we have to manage other requests, one or several internal logics of the component, etc.
On top of that, as the design and development progressed, we had to add new features, a cache system, data synchronization via websocket events, query parallelization, better error handling, ... All this would make the code even more complex and therefore less readable.
So I decided to look for different solutions to improve the user experience and the best development conditions.
A first solution would have been to refactor all our data retrieval implementations into the most generic custom hook possible and try to do the same for update and creation.
But while looking for examples of best practices regarding a custom hook in the genre, I came across some discussions and videos that talked about two libraries : React Query and . SWR. Seeing the enthusiasm that the React developer community had for these two libraries, I was eager to test them. After several tests, I preferred the features that React Query had to offer. So I made a presentation to the team and we decided to refactor our API requests using React Query.
1 - But what is React Query in concrete terms?
React Query is a library that provides several custom hooks to better organize your API requests, have a cache system already embedded and adjustable, synchronize your server states from anywhere in the application, and many others that I let you discover in their documentation.
I'm not going to make a complete tutorial of React Query. For that, I let you browse their documentation or search the many tutorials/presentations of the library that have already been made on the web.
2 - How is React Query used at Neomanis?
The default options
After playing a lot with the different hooks that React Query offers, we arrived at a rather stable implementation. First, we'll talk about staleTime and cacheTime. By default, React Query proposes a staleTime of 0, which means that a refetch of the data will be done automatically on a remount of the useQuery in question, on a refocus of the window or if the network has reconnected.
At Neomanis, we use a websocket to notify a user that his data is not up to date and that he needs to refetch some data. We have therefore set a higher staleTime and cacheTime to prevent too frequent refetches.
Our implementation of useQuery
For a better readability of the code, we decided to use only custom hooks containing the different hooks of React Query and the different logics we need.
For example, this is what an API request hook will look like when it retrieves the list of tickets linked to a user:
export function useTicketsRelativeToUser(userUid: string | undefined) {
return useQuery(["tickets", userUid], () => getTicketsRelativeToUser(userUid), {
enabled: !!userUid,
});
}
As the useruid will not always be defined, we use the enabled option of useQuery to make sure that the data is retrieved only when the useruid is set.
From time to time, we also use the select option of useQuery which allows us to change the form of the data we retrieve.
For example, here we get a list of categories that we format to be able to use it directly in one of our select inputs:
export function useCategories() {
return useQuery("categories", () => getGlpiCategories(), {
select: (data) => data.map((category) => ({ label: category.name, value: category.id })),
});
}
Our websocket system
As mentioned above, we use a websocket system that allows us to notify the user if one of his data is out of date (stale).
For example, we have a case where when another technician makes changes to a ticket that is linked to multiple users, the other users will receive a notification to report the presence of changes and switch it to the stale state. So the next time the user goes to the screen of the ticket in question, a refetch will be done to get the new updated data.
const queryClient = useQueryClient();
useEffect(() => {
socket.on(NotificationEvent.TICKET_UPDATE, (data) => {
queryClient.invalidateQueries(["ticket", data.objectId]);
});
return () => {
socket.off(NotificationEvent.TICKET_UPDATE);
};
}, []);
This allows us to have data displayed to the user that is as up-to-date as possible while making the fewest possible API requests!
What about creating and updating?
Well it's the same, we created custom hooks to handle each mutation.
Here is an example of how to create an answer to a technical question:
export function useCreateAnswer() {
const queryClient = useQueryClient();
const { toasterDispatch } = useContext(GlobalContext);
const { t } = useTranslation();
return useMutation(
({ text, user, technicalQuestionId }: CreateAnswer) =>
createAnswerForOneTechnicalQuestion(technicalQuestionId, { text, user }),
{
onSuccess: (result, { technicalQuestionId }) => {
queryClient.setQueryData<TechnicalQuestionAnswer[]>(["answers", technicalQuestionId], (oldAnswers) => [
result,
...(oldAnswers ?? []),
]);
},
onError: () => {
toasterDispatch({
type: "SIMPLE_MESSAGE",
payload: {
position: "top",
toasterMessage: t("error.somethingWentWrong"),
callBackEnd: () => toasterDispatch({ type: "RESET" }),
emotion: "sad",
},
});
},
}
);
}
We can see that we use the option onSuccess option to directly update the answer data, adding the new answer to the existing answer table.
In case of error, we have a toaster system that is managed by a useReducer in the context of the application, which will allow us to display a temporary error message to the user.
3 - Conclusion
React Query answers our original problem: Improve the user experience with easy-to-access features such as error handling and data synchronization.
At the same time, its implementation has resulted in a better development experience thanks to a more readable code and the fact that it is very easy to extract from the components all the logic related to API requests.
I hope that this feedback will have allowed you to have a small overview of the different possibilities that React Query offers, on our side we will continue to follow very closely the next evolutions of this library!
article difficult to follow because of the rather bad formulations, example:
- "This is a very simple use case, but it still requires three states for data, loader and error management"
Hello, thank you for your opinion.
The sentence you quote explains that, for a simple use case, which can be found in any application, there is already a complex code base. When there will be new features to add around this code, the task will be more and more difficult and the code will be less and less readable.
I hope that I have answered your question and perhaps you would have more details about other problems that you have encountered?
Raphaël