Skip to content

Commit 614aa26

Browse files
committed
chore(examples): optimistic updates
1 parent 899bd4c commit 614aa26

File tree

1 file changed

+142
-23
lines changed

1 file changed

+142
-23
lines changed

examples/05_mutation/src/App.tsx

Lines changed: 142 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,157 @@
1+
import React from 'react'
12
import { useAtom } from 'jotai/react'
2-
import { atomWithMutation } from 'jotai-tanstack-query'
3-
4-
const postAtom = atomWithMutation(() => ({
5-
mutationKey: ['posts'],
6-
mutationFn: async ({ title }: { title: string }) => {
7-
const res = await fetch(`https://jsonplaceholder.typicode.com/posts`, {
8-
method: 'POST',
9-
body: JSON.stringify({
10-
title,
11-
body: 'body',
12-
userId: 1,
13-
}),
14-
headers: {
15-
'Content-type': 'application/json; charset=UTF-8',
16-
},
17-
})
18-
const data = await res.json()
19-
return data
3+
import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query'
4+
5+
interface Post {
6+
id: number
7+
title: string
8+
body: string
9+
userId: number
10+
}
11+
12+
interface NewPost {
13+
title: string
14+
}
15+
16+
interface OptimisticContext {
17+
previousPosts: Post[] | undefined
18+
}
19+
20+
// Query to fetch posts list
21+
const postsQueryAtom = atomWithQuery(() => ({
22+
queryKey: ['posts'],
23+
queryFn: async () => {
24+
const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5')
25+
return res.json() as Promise<Post[]>
2026
},
2127
}))
2228

23-
const Posts = () => {
24-
const [{ mutate, status }] = useAtom(postAtom)
29+
// Mutation with optimistic updates
30+
const postAtom = atomWithMutation<Post, NewPost, Error, OptimisticContext>(
31+
(get) => {
32+
const queryClient = get(queryClientAtom)
33+
return {
34+
mutationKey: ['addPost'],
35+
mutationFn: async ({ title }: NewPost) => {
36+
// Randomly fail for testing error handling (30% failure rate)
37+
if (Math.random() < 0.3) {
38+
throw new Error('Randomly simulated API error')
39+
}
40+
41+
const res = await fetch(`https://jsonplaceholder.typicode.com/posts`, {
42+
method: 'POST',
43+
body: JSON.stringify({
44+
title,
45+
body: 'body',
46+
userId: 1,
47+
}),
48+
headers: {
49+
'Content-type': 'application/json; charset=UTF-8',
50+
},
51+
})
52+
const data = await res.json()
53+
return data as Post
54+
},
55+
// When mutate is called:
56+
onMutate: async (newPost: NewPost) => {
57+
// Cancel any outgoing refetches
58+
// (so they don't overwrite our optimistic update)
59+
await queryClient.cancelQueries({ queryKey: ['posts'] })
60+
61+
// Snapshot the previous value
62+
const previousPosts = queryClient.getQueryData<Post[]>(['posts'])
63+
64+
// Optimistically update to the new value
65+
queryClient.setQueryData<Post[]>(['posts'], (old) => {
66+
const optimisticPost: Post = {
67+
id: Date.now(), // Temporary ID
68+
title: newPost.title,
69+
body: 'body',
70+
userId: 1,
71+
}
72+
return old ? [...old, optimisticPost] : [optimisticPost]
73+
})
74+
75+
// Return a result with the snapshotted value
76+
return { previousPosts }
77+
},
78+
// If the mutation fails, use the result returned from onMutate to roll back
79+
onError: (
80+
_err: Error,
81+
_newPost: NewPost,
82+
onMutateResult: OptimisticContext | undefined
83+
) => {
84+
console.debug('onError', onMutateResult)
85+
if (onMutateResult?.previousPosts) {
86+
queryClient.setQueryData(['posts'], onMutateResult.previousPosts)
87+
}
88+
},
89+
// Always refetch after error or success:
90+
onSettled: (
91+
_data: Post | undefined,
92+
_error: Error | null,
93+
_variables: NewPost,
94+
_onMutateResult: OptimisticContext | undefined
95+
) => {
96+
queryClient.invalidateQueries({ queryKey: ['posts'] })
97+
},
98+
}
99+
}
100+
)
101+
102+
const PostsList = () => {
103+
const [{ data: posts, isPending }] = useAtom(postsQueryAtom)
104+
105+
if (isPending) return <div>Loading posts...</div>
106+
107+
return (
108+
<div>
109+
<h3>Posts:</h3>
110+
<ul>
111+
{posts?.map((post: Post) => (
112+
<li key={post.id}>{post.title}</li>
113+
))}
114+
</ul>
115+
</div>
116+
)
117+
}
118+
119+
const AddPost = () => {
120+
const [{ mutate, isPending, status }] = useAtom(postAtom)
121+
const [title, setTitle] = React.useState('')
122+
25123
return (
26124
<div>
27-
<button onClick={() => mutate({ title: 'foo' })}>Click me</button>
28-
<pre>{JSON.stringify(status, null, 2)}</pre>
125+
<div>
126+
<input
127+
value={title}
128+
onChange={(e) => setTitle(e.target.value)}
129+
placeholder="Enter post title"
130+
/>
131+
<button
132+
onClick={() => {
133+
if (title) {
134+
mutate({ title })
135+
setTitle('')
136+
}
137+
}}
138+
disabled={isPending}
139+
>
140+
{isPending ? 'Adding...' : 'Add Post'}
141+
</button>
142+
</div>
143+
<div>
144+
<strong>Status:</strong> {status}
145+
</div>
29146
</div>
30147
)
31148
}
32149

33150
const App = () => (
34151
<>
35-
<Posts />
152+
<h2>atomWithMutation with optimistic updates</h2>
153+
<PostsList />
154+
<AddPost />
36155
</>
37156
)
38157

0 commit comments

Comments
 (0)