On Next.js, React Query, and learning to love async/await

Josh Frank
Geek Culture
Published in
4 min readAug 25, 2021

React Query is a clever package of hooks and tools for the React/Next family of frameworks written by Tanner Linsley. I’m familiarizing myself with it because I’m expected to contribute to several web apps soon which all rely heavily on asynchronous data. Managing state with flying Promises and fetches everywhere makes my vertigo act up… but Next.js and React Query soothe the nausea a bit — with a learning curve.

Let’s say I have a to-do list app. The front end in Next has a list of tasks and a controlled form; when a user adds a task, the front end should update the DOM and update a back end with fetch( "...", { method: "POST" } ). The form looks a bit like this:

import { useState } from 'react';import homeStyles from '../styles/Home.module.css';const AddToDoForm = () => {  const [ toDoFormState, setToDoFormState ] = useState( "" );  return <form
className={ homeStyles.form }
onSubmit={ ...a fetch/post/state change of some kind... }
>
<input
value={ toDoFormState }
onChange={ changeEvent => setToDoFormState( changeEvent.target.value ) }
/>
<input type="submit" value="Do it!" />
</form>;
};export default AddToDoForm;

Without React Query, I might decide to create a global state in the App component, and then pass around getters and setters as props to keep that global state updated as I fetch. I might even set up a Redux store if I’m feeling frosty. But… that approach stinks with asynchronous data. Why waste grey hairs and lines of code making sure database and state are always dancing at the same tempo?

With React Query, I can fetch all the live-long day without ever worrying about global state at all. I just give my app a URL, a method, and some data, point to where/when an update should occur… and React Query does the rest. To make it happen, after installing with npm i react-query or yarn add react-query, I backtrack to the app’s index page and wrap the entire component in a QueryClientProvider:

import { QueryClient, QueryClientProvider } from 'react-query';import AddToDoForm from '../components/AddToDoForm';...const queryClient = new QueryClient();const ToDoCards = () => { ...component with a div for each task... }export default function Home() {  return <QueryClientProvider client={ queryClient }>
...
<AddToDoForm />
<ToDoCards />
</QueryClientProvider>;
}

And just like that, now that the Hero of Time has a queryClient, he gains the magical useQuery hook, giving him the ability to manage global state as he fetches:

import { QueryClient, QueryClientProvider, useQuery } from 'react-query';...const ToDoCards = () => {
const { isLoading, data } = useQuery( "todos", async () => {
const response = await fetch( `...` );
return response.json();
} );

return <div className={ homeStyles.grid }>
{ isLoading ? "Loading..." : data.map( todo => <ToDoCard
key={ todo.id }
todo={ todo }
allTodos={ data }
/> ) }
</div>;
}
export default function Home() { return ...; }

useQuery takes two arguments: a key, used to refer to this resource in the future, and a callback that actually fetches and returns some json(). useQuery also returns other variables to de-structure, like status and error messages; in this case, I’m using the isLoading boolean for a nice loading message where the cards live while the user waits for a response.

The React Query hook for making POST and PATCH requests is useMutation(). Let’s put it to work back in the AddToDoForm component:

...import { useMutation, useQueryClient } from 'react-query';const handleSubmission = async ( { toDoFormState } ) => {
const response = await fetch( `http://localhost:${ 3001 }/todos`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify( { ...new todo from ToDoFormState... } )
} );
return response.json();
};
const AddToDoForm = () => { const queryClient = new QueryClient(); const [ toDoFormState, setToDoFormState ] = useState( "" ); const { mutate } = useMutation( handleSubmission, {
onSuccess: async newToDo => {
setToDoFormState( "" );
await queryClient.cancelQueries( "todos" );
const previousValue = queryClient.getQueryData( "todos" );
queryClient.setQueryData( "todos", previousQueryData => [ ...previousQueryData, newToDo ] );
return previousValue;
},
} );
return <form
className={ homeStyles.form }
onSubmit={ async submissionEvent => {
submissionEvent.preventDefault();
try { mutate( { toDoFormState } ); }
catch ( error ) { console.log( error ); }
} }

>
<input
value={ toDoFormState }
onChange={ changeEvent => setToDoFormState( changeEvent.target.value ) }
/>
<input type="submit" value="Do it!" />
</form>;
};export default AddToDoForm;

To note:

  • Just as before, I’m returning json() fetched asynchronously — but notice in my handleSubmission function I’m de-structuring the toDoFormState from my controlled form in the arguments.
  • The mutate function is de-structured from useMutation, which takes two arguments: the handleSubmission callback I just defined which makes the fetch, and an object with one or more of three keys: onError, onSuccess, and/or onSettled (either error or success), each of which points to a function with some instructions for our queryClient for responding to each situation. For brevity’s sake, here I’m just defining an onSuccess method, where I 1) clear the form state, 2) setQueryData for our “todos” key to the ...previousQueryData with the new database entry tacked onto the end, and 3) return the previous query data.
  • Finally, I can call mutate( {} ) — making sure, of course, to wrap it in a try {} catch () {} just in case a fetch goes awry.

React Query isn’t falling off a log, but in an app with constant fetching, I’d argue it’s well worth the trouble. No state props like todosToDisplay or setTodosToDisplay… no need to worry about dispatch or store or context… and lots of other caching and scrolling hooks that improve app performance.

The code above lives here, and a great set of docs/examples are here.

--

--

Josh Frank
Josh Frank

Written by Josh Frank

Oh geez, Josh Frank decided to go to Flatiron? He must be insane…

No responses yet