Skip to main content

Tanstack React Query AsyncState Management

TST, Hongkong

Scaffolding

npm create vite@latest react-query
✔ Select a framework: › React
✔ Select a variant: › TypeScript + SWC
cd react-query
npm install
npm install @tanstack/react-query uuid vite-tsconfig-paths
npm install --save-dev @types/uuid

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

./tsconfig.json

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

./vite.config.js

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 HelloWorld from 'components/HelloWorld'

import 'styles/App.css'

export default function App() {

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

./src/components/HelloWorld.tsx

import React from 'react'

import { iHelloWorld } from 'types/interfaces'

export default function HelloWorld({ greeting }: iHelloWorld): React.JSX.Element {
return (
<h1>
{ greeting }
</h1>
)
}

./src/types/interfaces.ts

export interface iHelloWorld {
greeting: string
}
npm run dev

Adding React Query

Wrapping the App inside the Query Provider

./src/main.tsx

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

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

const queryClient = new QueryClient()

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

Build the Todo View

./src/views/App.tsx

import { useQuery } from '@tanstack/react-query'
import { fetchTodos } from 'api'

import Greeting from 'components/HelloWorld'
import TodoCard from 'components/TodoCard'
import 'styles/App.css'

export default function App() {
const { data: todos, isLoading } = useQuery({
queryFn: () => fetchTodos(),
queryKey: ["todos"]
})

if (isLoading) {
return <div>Loading ...</div>
}

return (
<>
<Greeting greeting='Hello from React Typescript' />

{todos?.map((todo) => {
return <TodoCard key={todo.id} todo={todo} />
})}

</>
)
}

Building a Mock-API

Adding a API component that allows us to fetch and add Todos that are then consumed by the view:

./src/api/index.ts

import {v4 as uuid} from 'uuid'

import { iTodo } from "types/interfaces"

const todos: iTodo[] = [
{
"id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"title": "Do something",
"completed": false
}
]

export const fetchTodos = async (query=''): Promise<iTodo[]> => {
await new Promise((resolve) => setTimeout(resolve, 1000))

console.log("INFO :: Todos were fetched")

const filteredTodos = todos.filter((todo) =>
todo.title.toLowerCase().includes(query.toLocaleLowerCase())
)

return [... filteredTodos]
}


export const addTodo = async (todo: Pick<iTodo, 'title'>): Promise<iTodo> => {
await new Promise((resolve) => setTimeout(resolve, 1000))

const newTodo = {
id: uuid(),
title: todo.title,
completed: false
}

todos.push(newTodo)

return newTodo
}

Fetch Query

Add the Todo Template Component

./src/components/TodoCard.tsx

import React, {useState} from 'react'

import { iTodo } from 'types/interfaces'

interface TodoProps {
todo: iTodo;
}

export default function TodoCard({ todo }: TodoProps): React.JSX.Element {
const [checked, setChecked] = useState(todo.completed);

return (
<div>
{todo.title}
<input
type="checkbox"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
/>
</div>
);
}

./src/styles/interfaces.ts

export interface iTodo {
id: string
title: string
completed: boolean
}

Tanstack ReactQuery

Mutations

Update the Server State

Our Mock API already provides a function to add more todos. We can now use the ReactQuery state mutation to add a todo and also refresh the client side state updating the displayed list:

./src/views/App.tsx

import { useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'

import Greeting from 'components/HelloWorld'
import TodoCard from 'components/TodoCard'
import { addTodo, fetchTodos } from 'api'

import 'styles/App.css'

export default function App() {
const queryClient = useQueryClient()
const [title, setTitle] = useState('')

const { data: todos, isLoading } = useQuery({
queryFn: () => fetchTodos(),
queryKey: ["todos"]
})

const { mutateAsync: addTodoMutation } = useMutation({
mutationFn: addTodo,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
})

if (isLoading) {
return <div>Loading ...</div>
}

return (
<>
<Greeting greeting='Hello from React Typescript' />

{ todos?.map((todo) => {
return <TodoCard key={todo.id} todo={todo} />
})
}

<input
type='text'
onChange={(e) => setTitle(e.target.value)}
value={title}
/>

<button onClick={ async () => {
try {
await addTodoMutation({ title })
setTitle('')
} catch (e) {
console.log(e)
}
}}>
Add Todo
</button>

</>
)
}

Tanstack ReactQuery

Filter Todos

./src/views/App.tsx

import { useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'

import Greeting from 'components/HelloWorld'
import TodoCard from 'components/TodoCard'
import { addTodo, fetchTodos } from 'api'

import 'styles/App.css'

export default function App() {
const queryClient = useQueryClient()

const [search, setSearch] = useState('')
const [title, setTitle] = useState('')

const { data: todos, isLoading } = useQuery({
queryFn: () => fetchTodos(search),
queryKey: ["todos", {search}]
})

const { mutateAsync: addTodoMutation } = useMutation({
mutationFn: addTodo,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
})

if (isLoading) {
return <div>Loading ...</div>
}

return (
<>
<Greeting greeting='Hello from React Typescript' />

{ todos?.map((todo) => {
return <TodoCard key={todo.id} todo={todo} />
})
}

<input
type='text'
onChange={(e) => setTitle(e.target.value)}
value={title}
/>

<button onClick={ async () => {
try {
await addTodoMutation({ title })
setTitle('')
} catch (e) {
console.log(e)
}
}}>
Add Todo
</button>

<input
type='text'
onChange={e => (setSearch(e.target.value))}
value={search}
/>

</>
)
}

Tanstack ReactQuery

Query Debounce

This works - but is very painful to use due to the simulated delay from the API response. Adding a custom hook to debounce the search input:

./src/utils/hooks.ts

import { useEffect, useState } from "react"

export const useDebounce = <T>(value: T, delay: 500) => {
const [debounce, setDebounce] = useState<T> (value)

useEffect( () => {
const timeout = setTimeout(() => {
setDebounce(value)
}, delay)

return () => clearTimeout(timeout)
}, [value, delay])

return debounce
}

./src/views/App.tsx

import { useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'

import Greeting from 'components/HelloWorld'
import TodoCard from 'components/TodoCard'
import { useDebounce } from 'utils/hooks'
import { addTodo, fetchTodos } from 'api'

import 'styles/App.css'

export default function App() {
const queryClient = useQueryClient()

const [title, setTitle] = useState('')
const [search, setSearch] = useState('')
const debounceSearch = useDebounce(search, 500)

const { data: todos, isLoading } = useQuery({
queryFn: () => fetchTodos(debounceSearch),
queryKey: ["todos", {debounceSearch}]
})

const { mutateAsync: addTodoMutation } = useMutation({
mutationFn: addTodo,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
})

if (isLoading) {
return <div>Loading ...</div>
}

return (
<>
<Greeting greeting='Hello from React Typescript' />

{ todos?.map((todo) => {
return <TodoCard key={todo.id} todo={todo} />
})
}

<input
type='text'
onChange={(e) => setTitle(e.target.value)}
value={title}
placeholder='Todo'
/>

<button onClick={ async () => {
try {
await addTodoMutation({ title })
setTitle('')
} catch (e) {
console.log(e)
}
}}>
Add Todo
</button>

<input
type='text'
onChange={e => (setSearch(e.target.value))}
value={search}
placeholder='Search'
/>

</>
)
}