Skip to main content

React Query AsyncState Management

TST, Hongkong

Scaffolding

npm create vite@latest react-query-mutation
✔ Select a framework: › React
✔ Select a variant: › TypeScript + SWC
cd react-query-mutation
npm install
npm install react-query

Use baseURL to be able to work with absolute imports relative to the defined base:

./tsconfig.json

{
"compilerOptions": {
"baseUrl": "./src",
...

./vite.config.js npm install vite-tsconfig-paths

import { defineConfig } from 'vite'
import tsconfigPaths from 'vite-tsconfig-paths'
import react from '@vitejs/plugin-react-swc'

export default defineConfig({
plugins: [
react(),
tsconfigPaths()
],
})

./src/views/App.tsx

import Users from 'components/Users'

import 'styles/App.css'

export default function App() {

return (
<>
<Users greeting='Hello from React Typescript' />
</>
)
}

./src/components/Users.tsx

import React from 'react'

import { iUsersProps } from 'types/interfaces'

export default function Users({ greeting }: iUsersProps): React.JSX.Element {
return (
<h1>
{ greeting }
</h1>
)
}
npm run dev

Backend API

npm install json-server

./api/db.json

{
"users": [
{
"id": "1",
"name": "Player1",
"tier": 3
},
{
"id": "2",
"name": "Player2",
"tier": 2
}
]
}

./package.json

"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"api": "json-server api/db.json",
"preview": "vite preview"
},
npm run api

Adding React Query

Add the Query Provider to the entry point of your app:

./src/main.tsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import { QueryClientProvider, QueryClient } from 'react-query'
import { ReactQueryDevtools } from 'react-query/devtools'

import App from 'views/App.tsx'
import 'styles/index.css'

const reactQueryClient = new QueryClient({
defaultOptions: {
queries: {
// use cache for 5s before refetching data
staleTime: 5 * 1000
}
}
})

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={reactQueryClient}>
<ReactQueryDevtools initialIsOpen={false} />
<App />
</QueryClientProvider>
</React.StrictMode>,
)

Now we can use ReactQuery to retrieve data from our JSON API and render it:

./src/components/Users.tsx

import React from 'react'
import { useQuery, UseQueryResult } from 'react-query'

import { iUsersProps, iUser } from 'types/interfaces'

const apiUrl = 'http://localhost:3000/users'

const fetchUsers = async(): Promise<iUser[]> => {
const res = await fetch(apiUrl)
if (res.ok) {
return res.json()
}
throw new Error('ERROR :: Data fetching failed!')
}

export default function UsersList({ greeting }: iUsersProps): React.JSX.Element {
const { isLoading, isError, error, data }: UseQueryResult<iUser[], Error> = useQuery<iUser[], Error>(
'users', fetchUsers, {
// use cached values for 20s before re-running query
staleTime: 20 * 1000
})

if (isLoading) {
return (
<>
<h1>{ greeting }</h1>
<p>loading...</p>
</>
)
}

if (isError) return <p>ERROR :: {error?.message}</p>

return (
<>
<h2>All Users:</h2>
<ul>
{
data?.map((user) => {
return(
<div key={user.id}>
<li><strong>ID:</strong>{user.id}</li>
<li><strong>Username:</strong>{user.name}</li>
<li><strong>Tier:</strong>{user.tier}</li>
<hr/>
</div>
)
})
}
</ul>
</>
)
}

Working with Query Params

The implementation above retrieves all users und displays them in a list. The json server also provides support for query parameter to retrieve a single entry.

./src/components/User.tsx

import React from 'react'
import { useQuery } from 'react-query'

import { iUser } from 'types/interfaces'

const apiUrl = 'http://localhost:3000/users/'
const id = '1' // pretend id was extracted from URL param

const fetchUserById = async(id: string | undefined): Promise<iUser> => {
if (typeof id === 'string') {
const res = await fetch(apiUrl+id)
if (res.ok) {
return res.json()
}
throw new Error('ERROR :: Data fetching failed!')
}
throw new Error('ERROR :: Invalid User ID!')
}

export default function UserDetails(): React.JSX.Element {
const { isLoading, isError, error, data } = useQuery<iUser, Error>(
['user', id], () => fetchUserById(id), {
// use cached values for 20s before re-running query
staleTime: 20 * 1000,
// only run query when id is defined
enabled: !!id
})

if (isLoading) {
return <p>loading...</p>
}

if (isError) return <p>ERROR :: {error?.message}</p>

return (
<>
<h2>First User:</h2>
<ul>
<li><strong>ID:</strong>{data?.id}</li>
<li><strong>Username:</strong>{data?.name}</li>
<li><strong>Tier:</strong>{data?.tier}</li>
</ul>
</>
)
}

React Query AsyncState Management

Mutations

Using regular fetch works but has the disadvantage that we don't see a refresh of our list component when we refresh:

import React from 'react'
import { v4 as uuidv4 } from 'uuid'

import {fetchUsers} from 'components/Users'

const apiUrl = 'http://localhost:3000/users/'

const createUser = async(): Promise<string> => {

const usernames = ['Player3','Player4','Player5','Player6']

const response = await fetch(apiUrl, {
method: 'POST',
body: JSON.stringify({
id: uuidv4(),
name: usernames[Math.floor(Math.random() * 4)],
tier: Math.floor(Math.random() * 4)
})
})
fetchUsers()
return response.statusText
}

export function AddUser(): React.JSX.Element {

return (
<>
<button type="submit" onClick={createUser}>
Create New User (using regular fetch())
</button>
</>
)
}

Using useMutation instead allows us to use queryClient.invalidateQueries('users') to force a refresh of the users query which triggers a reload of the list component:

import React, { FormEventHandler } from 'react'
import { v4 as uuidv4 } from 'uuid'

import { iUser } from 'types/interfaces'
import { useMutation, UseMutationResult, useQueryClient } from 'react-query'

const apiUrl = 'http://localhost:3000/users/'

const createUser = async(name: string, tier: number): Promise<iUser> => {

const response: Response = await fetch(apiUrl, {
method: 'POST',
body: JSON.stringify({
id: uuidv4(),
name,
tier
})
})
if (response.ok) {
return response.json()
}
throw new Error('ERROR :: User could not be created!')
}

export default function AddUserMutation(): React.JSX.Element {

const queryClient = useQueryClient()

const mutation:UseMutationResult<iUser, Error, void> = useMutation<iUser, Error>(
async ({ name, tier }) => createUser(name, tier), {
onSuccess: (data: iUser) => {
queryClient.invalidateQueries('users')
}
}
)

const onSubmit: FormEventHandler<HTMLFormElement> = async (event: React.SyntheticEvent) => {
event.preventDefault()

const target = event.target as typeof event.target & {
name: { value: string }
tier: { value: number }
}

const name = target.name.value
const tier = target.tier.value
mutation.mutate({ name, tier })

return
}

return (
<>
{mutation.isLoading ? (
<p>Adding todo</p>
) : (
<>
{ mutation.isError ? <div>ERROR :: {mutation?.error?.message}</div> : null }

{ mutation.isSuccess ? (
<div>
User added :: Name: {mutation?.data?.name} & Tier: {mutation?.data?.tier}
</div>
) : null }
</>
)}

<form onSubmit={onSubmit}>
<label htmlFor="name">Name:</label>
<br />
<input type="text" id="name" name="name" />
<br />
<label htmlFor="tier">Tier:</label>
<br />
<select name="tier" id="tier">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<br />
<br />
<input type="submit" value="Submit" />
</form>
</>
)
}