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