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>
        </>
    )
}