React Query AsyncState Management
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>
</>
)
}
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>
</>
)
}