Integrating the backend service using TanStack Query
After finishing creating all the UI components, we can now move on to integrating them with the backend we created in the previous chapter. For the integration, we are going to use TanStack Query (previously called React Query), which is a data fetching library that can also help us with caching, synchronizing, and updating data from a backend.
TanStack Query specifically focuses on managing the state of fetched data (server state). While other state management libraries can also deal with server state, they specialize in managing client state instead. Server state has some stark differences from client state, such as the following:
- Being persisted remotely in a location the client does not control directly
- Requiring asynchronous APIs to fetch and update state
- Having to deal with shared ownership, which means that other people can change the state without your knowledge
- State becoming stale (“out of date”) at some point when changed by the server or other people
These challenges with server state result in issues such as having to cache, deduplicate multiple requests, update “out of date” state in the background, and so on.
TanStack Query provides solutions to these issues out of the box and thus makes dealing with server state simple. You can always combine it with other state management libraries that focus on client state as well. For use cases where the client state essentially just reflects the server state though, TanStack Query on its own can be good enough as a state management solution!
Note
The reason why React Query got renamed to TanStack Query is that the library now also supports other frameworks, such as Solid, Vue, and Svelte!
Now that you know why and how TanStack Query can help us integrate our frontend with the backend, let’s get started using it!
Setting up TanStack Query for React
To set up TanStack Query, we first have to install the dependency and set up a query client. The query client is provided to React through a context and will store information about active requests, cached results, when to periodically re-fetch data, and everything needed for TanStack Query to function.
Let’s get started setting it up now:
- Open a new Terminal (do not quit Vite!) and install the
@tanstack/react-query
dependency by running the following command in the root of our project:$ npm install @tanstack/react-query@5.12.2
We are now going to move our current
App
component to a newBlog
component, as we are going to use theApp
component for setting up libraries and contexts instead. - Rename the
src/App.jsx
file tosrc/Blog.jsx
.Do not update imports yet. If VS Code asks you to update imports, click No.
- Now, in
src/Blog.jsx
, change the function name fromApp
toBlog
:export function Blog() {
- Create a new
src/App.jsx
file. In this file, importQueryClient
andQueryClientProvider
from TanStack React Query:import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
- Also, import the
Blog
component:import { Blog } from './Blog.jsx'
- Now, create a new query client:
const queryClient = new QueryClient()
- Define the
App
component and render theBlog
component wrapped insideQueryClientProvider
:export function App() { return ( <QueryClientProvider client={queryClient}> <Blog /> </QueryClientProvider> ) }
That’s all there is to setting up TanStack Query! We can now make use of it inside our Blog
component (and its children).
Fetching blog posts
The first thing we should do is fetch the list of blog posts from our backend. Let’s implement that now:
- First of all, in the second Terminal window opened (not where Vite is running), run the backend server (do not quit Vite!), as follows:
$ cd backend/ $ npm start
If you get an error, make sure Docker and MongoDB are running properly!
Tip
If you want to develop the backend and frontend at the same time, you can start the backend using npm run dev
to make sure it hot reloads when you change the code.
- Create a
.env
file in the root of the project, and enter the following contents into it:VITE_BACKEND_URL="http://localhost:3001/api/v1"
Vite supports
dotenv
out of the box. All environment variables that should be available to be accessed within the frontend need to be prefixed withVITE_
. Here, we set an environment variable to point to our backend server. - Create a new
src/api/posts.js
file. In this file, we are going to define a function to fetch posts, which accepts the query params for the/posts
endpoint as an argument. These query params are used to filter by author and tag and define sorting usingsortBy
andsortOrder
:export const getPosts = async (queryParams) => {
- Remember that we can use the
fetch
function to make a request to a server. We need to pass the environment variable to it and add the/posts
endpoint. After the path, we add query params, which are prefixed with the?
symbol:const res = await fetch( `${import.meta.env.VITE_BACKEND_URL}/posts?` +
- Now we need to use the
URLSearchParams
class to turn an object into query params. That class will automatically escape the input for us and turn it into valid query params:new URLSearchParams(queryParams),
- Like we did before in the browser, we need to parse the response as JSON:
) return await res.json() }
- Edit
src/Blog.jsx
and remove the sampleposts
array:const posts = [ { title: 'Full-Stack React Projects', contents: "Let's become full-stack developers!", author: 'Daniel Bugl', }, { title: 'Hello React!' }, ]
- Also, import the
useQuery
function from@tanstack/react-query
and thegetPosts
function from ourapi
folder in thesrc/Blog.jsx
file:import { useQuery } from '@tanstack/react-query' import { PostList } from './components/PostList.jsx' import { CreatePost } from './components/CreatePost.jsx' import { PostFilter } from './components/PostFilter.jsx' import { PostSorting } from './components/PostSorting.jsx' import { getPosts } from './api/posts.js'
- Inside the
Blog
component, define auseQuery
hook:export function Blog() { const postsQuery = useQuery({ queryKey: ['posts'], queryFn: () => getPosts(), })
The
queryKey
is very important in TanStack Query, as it is used to uniquely identify a request, among other things, for caching purposes. Always make sure to use unique query keys. Otherwise, you might see requests not triggering properly.For the
queryFn
option, we just call thegetPosts
function, without query params for now. - After the
useQuery
hook, we get the posts from our query and fall back to an empty array if the posts are not loaded yet:const posts = postsQuery.data ?? []
- Check your browser, and you will see that the posts are now loaded from our backend!
Now that we have successfully fetched blog posts, let’s get the filters and sorting working!
Implementing filters and sorting
To implement filters and sorting, we need to handle some local state and pass it as query params to postsQuery
. Let’s do that now:
- We start by editing the
src/Blog.jsx
file and importing theuseState
hook from React:import { useState } from 'react'
- Then we add state hooks for the
author
filter and the sorting options inside theBlog
component, before theuseQuery
hook:const [author, setAuthor] = useState('') const [sortBy, setSortBy] = useState('createdAt') const [sortOrder, setSortOrder] = useState('descending')
- Then, we adjust
queryKey
to contain the query params (so that whenever a query param changes, TanStack Query will re-fetch unless the request is already cached). We also adjustqueryFn
to callgetPosts
with the relevant query params:const postsQuery = useQuery({ queryKey: ['posts', { author, sortBy, sortOrder }], queryFn: () => getPosts({ author, sortBy, sortOrder }), })
- Now pass the values and relevant
onChange
handlers to the filter and sorting components:<PostFilter field='author' value={author} onChange={(value) => setAuthor(value)} /> <br /> <PostSorting fields={['createdAt', 'updatedAt']} value={sortBy} onChange={(value) => setSortBy(value)} orderValue={sortOrder} onOrderChange={(orderValue) => setSortOrder(orderValue)} />
Note
For simplicity’s sake, we are only using state hooks for now. A state management solution or context could make dealing with filters and sorting much easier, especially for larger applications. For our small blog application, it is fine to use state hooks though, as we are focusing mostly on the integration of the backend and frontend.
- Now, edit
src/components/PostFilter.jsx
and add thevalue
andonChange
props:export function PostFilter({ field, value, onChange }) { return ( <div> <label htmlFor={`filter-${field}`}>{field}: </label> <input type='text' name={`filter-${field}`} id={`filter-${field}`} value={value} onChange={(e) => onChange(e.target.value)} /> </div> ) } PostFilter.propTypes = { field: PropTypes.string.isRequired, value: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, }
- We also do the same for
src/components/PostSorting.jsx
:export function PostSorting({ fields = [], value, onChange, orderValue, onOrderChange, }) { return ( <div> <label htmlFor='sortBy'>Sort By: </label> <select name='sortBy' id='sortBy' value={value} onChange={(e) => onChange(e.target.value)} > {fields.map((field) => ( <option key={field} value={field}> {field} </option> ))} </select> {' / '} <label htmlFor='sortOrder'>Sort Order: </label> <select name='sortOrder' id='sortOrder' value={orderValue} onChange={(e) => onOrderChange(e.target.value)} > <option value={'ascending'}>ascending</option> <option value={'descending'}>descending</option> </select> </div> ) } PostSorting.propTypes = { fields: PropTypes.arrayOf(PropTypes.string).isRequired, value: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, orderValue: PropTypes.string.isRequired, onOrderChange: PropTypes.func.isRequired, }
- In your browser, enter
Daniel Bugl
as the author. You should see TanStack Query re-fetch the posts from the backend as you type, and once a match is found, the backend will return all posts by that author! - After testing it out, make sure to clear the filter again, so that newly created posts are not filtered by the author anymore later on.
Tip
If you do not want to make that many requests to the backend, make sure to use a debouncing state hook, such as useDebounce
, and then pass only the debounced value to the query param. If you are interested in gaining further knowledge about the useDebounce
hook and other useful hooks, I recommend checking out my book titled Learn React Hooks.
The application should now look as follows, with the posts being filtered by the author entered in the field, and sorted by the selected field, in the selected order:
Figure 4.4 – Our first full-stack application – a frontend fetching posts from a backend!
Now that sorting and filtering are working properly, let’s learn about mutations, which allow us to make requests to the server that change the state of the backend (for example, inserting or updating entries in the database).
Creating new posts
We are now going to implement a feature to create posts. To do this, we need to use the useMutation
hook from TanStack Query. While queries are meant to be idempotent (meaning that calling them multiple times should not affect the result), mutations are used to create/update/delete data or perform operations on the server. Let’s get started using mutations to create new posts now:
- Edit
src/api/posts.js
and define a newcreatePost
function, which accepts apost
object as an argument:export const createPost = async (post) => {
- We also make a request to the
/posts
endpoint, like we did forgetPosts
:const res = await fetch(`${import.meta.env.VITE_BACKEND_URL}/posts`, {
- However, now we also set
method
to aPOST
request, pass a header to tell the backend that we will be sending a JSON body, and then send ourpost
object as a JSON string:method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(post),
- Like with
getPosts
, we also need to parse the response as JSON:}) return await res.json() }
After defining the
createPost
API function, let’s use it in theCreatePost
component by creating a new mutation hook there. - Edit
src/components/CreatePost.jsx
and import theuseMutation
hook from@tanstack/react-query
, theuseState
hook from React, and ourcreatePost
API function:import { useMutation } from '@tanstack/react-query' import { useState } from 'react' import { createPost } from '../api/posts.js'
- Inside the
CreatePost
component, define state hooks fortitle
,author
, andcontents
:const [title, setTitle] = useState('') const [author, setAuthor] = useState('') const [contents, setContents] = useState('')
- Now, define a mutation hook. Here, we are going to call our
createPost
function:const createPostMutation = useMutation({ mutationFn: () => createPost({ title, author, contents }), })
- Next, we are going to define a
handleSubmit
function, which will prevent the default submit action (which refreshes the page), and instead call.mutate()
to execute the mutation:const handleSubmit = (e) => { e.preventDefault() createPostMutation.mutate() }
- We add the
onSubmit
handler to our form:<form onSubmit={handleSubmit}>
- We also add the
value
andonChange
props to our fields, as we did before for the sorting and filters:<div> <label htmlFor='create-title'>Title: </label> <input type='text' name='create-title' id='create-title' value={title} onChange={(e) => setTitle(e.target.value)} /> </div> <br /> <div> <label htmlFor='create-author'>Author: </label> <input type='text' name='create-author' id='create-author' value={author} onChange={(e) => setAuthor(e.target.value)} /> </div> <br /> <textarea value={contents} onChange={(e) => setContents(e.target.value)} />
- For the submit button, we make sure it says
Creating…
instead ofCreate
while we are waiting for the mutation to finish, and we also disable the button if no title was set (as it is required), or if the mutation is currently pending:<br /> <br /> <input type='submit' value={createPostMutation.isPending ? 'Creating...' : 'Create'} disabled={!title || createPostMutation.isPending} />
- Lastly, we add a message below the submit button, which will be shown if the mutation is successful:
{createPostMutation.isSuccess ? ( <> <br /> Post created successfully! </> ) : null} </form>
Note
In addition to isPending
and isSuccess
, mutations also return isIdle
(when the mutation is idle or in a fresh/reset state) and isError
states. The same states can also be accessed from queries, for example, to show a loading animation while posts are fetching.
- Now we can try adding a new post, and it seems to work fine, but the post list is not updating automatically, only after a refresh!
The issue is that the query key did not change, so TanStack Query does not refresh the list of posts. However, we also want to refresh the list when a new post is created. Let’s fix that now.
Invalidating queries
To ensure that the post list is refreshed after creating a new post, we need to invalidate the query. We can make use of the query client to do this. Let’s do it now:
- Edit
src/components/CreatePost.jsx
and import theuseQueryClient
hook:import { useMutation, useQueryClient } from '@tanstack/react-query'
- Use the query client to invalidate all queries starting with the
'posts'
query key. This will work with any query params to thegetPosts
request, as it matches all queries starting with'posts'
in the array:const queryClient = useQueryClient() const createPostMutation = useMutation({ mutationFn: () => createPost({ title, author, contents }), onSuccess: () => queryClient.invalidateQueries(['posts']), })
Try creating a new post, and you will see that it works now, even with active filters and sorting! As we can see, TanStack Query is great for handling server state with ease.