snapshot of https://github.com/TanStack/query.git (main, 2026-09-16; blobs > 131072B omitted)
Some checks failed
autofix.ci / autofix (push) Failing after 4s
Release / Release (push) Has been skipped
GitHub Actions Security Analysis / Run zizmor (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-16 01:13:08 +05:00
commit 3a013ccdfe
3570 changed files with 490194 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
// @ts-check
/** @type {import('eslint').Linter.Config} */
const config = {}
module.exports = config

View File

@@ -0,0 +1,6 @@
# Example
To run this example:
- `pnpm install`
- `pnpm dev`

View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" />
<title>Solid App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="/src/index.tsx" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,28 @@
{
"name": "@tanstack/query-example-solid-offline",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/query-async-storage-persister": "^5.102.8",
"@tanstack/solid-query": "^5.102.8",
"@tanstack/solid-query-devtools": "^5.102.8",
"@tanstack/solid-query-persist-client": "^5.102.8",
"msw": "^2.6.6",
"solid-js": "^1.9.7"
},
"devDependencies": {
"typescript": "5.8.3",
"vite": "^6.4.1",
"vite-plugin-solid": "^2.11.6"
},
"msw": {
"workerDirectory": [
"public"
]
}
}

View File

@@ -0,0 +1,208 @@
import { For, Match, Show, Switch, createSignal } from 'solid-js'
import { MutationCache, QueryClient, useQuery } from '@tanstack/solid-query'
import { SolidQueryDevtools } from '@tanstack/solid-query-devtools'
import { PersistQueryClientProvider } from '@tanstack/solid-query-persist-client'
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
import * as api from './api'
import { movieKeys, useMovie } from './movies'
const persister = createAsyncStoragePersister({
storage: window.localStorage,
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
staleTime: 2000,
retry: 0,
},
},
// configure global cache callbacks to show notifications
mutationCache: new MutationCache({
onSuccess: (data: any) => {
showNotification(data.message, 'success')
},
onError: (error) => {
showNotification(error.message, 'error')
},
}),
})
// we need a default mutation function so that paused mutations can resume after a page reload
queryClient.setMutationDefaults(movieKeys.all(), {
mutationFn: async ({ id, comment }: { id: string; comment: string }) => {
// to avoid clashes with our optimistic update when an offline mutation continues
await queryClient.cancelQueries({ queryKey: movieKeys.detail(id) })
return api.updateMovie(id, comment)
},
})
function showNotification(message: string, type: 'success' | 'error') {
const el = document.createElement('div')
el.style.cssText = `
position: fixed; top: 16px; right: 16px; z-index: 9999;
padding: 12px 24px; border-radius: 4px; color: white;
background: ${type === 'success' ? '#22c55e' : '#ef4444'};
animation: fadeIn 0.3s ease-in;
`
el.textContent = message
document.body.appendChild(el)
setTimeout(() => el.remove(), 3000)
}
export default function App() {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister }}
onSuccess={() => {
// resume mutations after initial restore from localStorage was successful
queryClient.resumePausedMutations().then(() => {
queryClient.invalidateQueries()
})
}}
>
<Movies />
<SolidQueryDevtools initialIsOpen />
</PersistQueryClientProvider>
)
}
function Movies() {
const [page, setPage] = createSignal<
{ type: 'list' } | { type: 'detail'; movieId: string }
>({ type: 'list' })
return (
<Switch>
<Match when={page().type === 'list'}>
<List
onSelectMovie={(movieId) => setPage({ type: 'detail', movieId })}
/>
</Match>
<Match when={page().type === 'detail'}>
<Detail
movieId={(page() as { type: 'detail'; movieId: string }).movieId}
onBack={() => setPage({ type: 'list' })}
/>
</Match>
</Switch>
)
}
function List(props: { onSelectMovie: (movieId: string) => void }) {
const moviesQuery = useQuery(() => ({
queryKey: movieKeys.list(),
queryFn: api.fetchMovies,
}))
return (
<Switch>
<Match when={moviesQuery.isLoading}>Loading...</Match>
<Match when={moviesQuery.data}>
<div>
<h1>Movies</h1>
<p>
Try to mock offline behavior with the button in the devtools. You
can navigate around as long as there is already data in the cache.
You'll get a refetch as soon as you go online again.
</p>
<ul>
<For each={moviesQuery.data!.movies}>
{(movie) => (
<li>
<a
href="#"
onClick={(e) => {
e.preventDefault()
props.onSelectMovie(movie.id)
}}
>
{movie.title}
</a>
</li>
)}
</For>
</ul>
<div>
Updated at: {new Date(moviesQuery.data!.ts).toLocaleTimeString()}
</div>
<Show when={moviesQuery.isFetching}>
<div>fetching...</div>
</Show>
</div>
</Match>
</Switch>
)
}
function Detail(props: { movieId: string; onBack: () => void }) {
const { comment, setComment, updateMovie, movieQuery } = useMovie(
props.movieId,
)
function submitForm(event: SubmitEvent) {
event.preventDefault()
updateMovie.mutate({
id: props.movieId,
comment: comment(),
})
}
return (
<Switch>
<Match when={movieQuery.isLoading}>Loading...</Match>
<Match when={movieQuery.data}>
<form onSubmit={submitForm}>
<a
href="#"
onClick={(e) => {
e.preventDefault()
props.onBack()
}}
>
Back
</a>
<h1>Movie: {movieQuery.data!.movie.title}</h1>
<p>
Try to mock offline behavior with the button in the devtools, then
update the comment. The optimistic update will succeed, but the
actual mutation will be paused and resumed once you go online again.
</p>
<p>
You can also reload the page, which will make the persisted mutation
resume, as you will be online again when you "come back".
</p>
<p>
<label>
Comment: <br />
<textarea
name="comment"
value={comment() ?? ''}
onInput={(event) => setComment(event.currentTarget.value)}
/>
</label>
</p>
<button type="submit">Submit</button>
<div>
Updated at: {new Date(movieQuery.data!.ts).toLocaleTimeString()}
</div>
<Show when={movieQuery.isFetching}>
<div>fetching...</div>
</Show>
<div>
{updateMovie.isPaused
? 'mutation paused - offline'
: updateMovie.isPending && 'updating...'}
</div>
</form>
</Match>
<Match when={movieQuery.isPaused}>
We're offline and have no data to show :(
</Match>
</Switch>
)
}

View File

@@ -0,0 +1,84 @@
import { HttpResponse, delay, http, passthrough } from 'msw'
import { setupWorker } from 'msw/browser'
const movies = [
{
id: '1',
title: 'Guardians of the Galaxy',
comment: '',
},
{
id: '2',
title: 'Wall-E',
comment: '',
},
]
export const fetchMovie = async (
id: string,
): Promise<{
ts: number
movie: { comment: string; id: string; title: string }
}> => {
const response = await fetch(`/movies/${id}`)
return await response.json()
}
export const fetchMovies = async (): Promise<{
ts: number
movies: typeof movies
}> => {
const response = await fetch('/movies')
return await response.json()
}
export const updateMovie = async (id: string, comment: string) => {
const response = await fetch(`/movies/${id}`, {
method: 'POST',
body: JSON.stringify({ comment }),
headers: { 'Content-Type': 'application/json' },
})
return await response.json()
}
export const worker = setupWorker(
http.get('/movies', async () => {
await delay(500)
return HttpResponse.json({
ts: Date.now(),
movies: movies.map(({ id, title }) => ({ id, title })),
})
}),
http.get('/movies/:id', async ({ params }) => {
const { id } = params
const movie = movies.find((movie) => movie.id === id)
if (!movie) {
return new HttpResponse(`Movie with id ${id} not found`, {
status: 404,
})
}
await delay(500)
return HttpResponse.json({ ts: Date.now(), movie })
}),
http.post('/movies/:id', async ({ request, params }) => {
const { id } = params
const body = (await request.json()) as { comment: string }
const { comment } = body
movies.forEach((movie) => {
if (movie.id === id) {
movie.comment = comment.toUpperCase()
}
})
await delay(500)
return HttpResponse.json({ message: `Successfully updated movie ${id}` })
}),
http.get('*.js', () => passthrough()),
http.get('*.svg', () => passthrough()),
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,18 @@
/* @refresh reload */
import { render } from 'solid-js/web'
import App from './App'
import { worker } from './api'
worker.start()
const root = document.getElementById('root')
if (!root) throw new Error('Missing #root element')
render(
() => (
<div style={{ padding: '16px' }}>
<App />
</div>
),
root,
)

View File

@@ -0,0 +1,57 @@
import { createSignal } from 'solid-js'
import { useMutation, useQuery, useQueryClient } from '@tanstack/solid-query'
import * as api from './api'
export const movieKeys = {
all: () => ['movies'],
list: () => [...movieKeys.all(), 'list'],
details: () => [...movieKeys.all(), 'detail'],
detail: (id: string) => [...movieKeys.details(), id],
}
export const useMovie = (movieId: string) => {
const queryClient = useQueryClient()
const movieQuery = useQuery(() => ({
queryKey: movieKeys.detail(movieId),
queryFn: () => api.fetchMovie(movieId),
}))
const [comment, setComment] = createSignal<string | undefined>()
const updateMovie = useMutation(() => ({
mutationKey: movieKeys.detail(movieId),
onMutate: async () => {
await queryClient.cancelQueries({ queryKey: movieKeys.detail(movieId) })
const previousData = queryClient.getQueryData<
Awaited<ReturnType<typeof api.fetchMovie>>
>(movieKeys.detail(movieId))
// remove local state so that server state is taken instead
setComment(undefined)
queryClient.setQueryData(movieKeys.detail(movieId), {
...previousData,
movie: {
...previousData?.movie,
comment: comment(),
},
})
return { previousData }
},
onError: (_: any, __: any, context: any) => {
queryClient.setQueryData(movieKeys.detail(movieId), context.previousData)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: movieKeys.detail(movieId) })
},
}))
return {
comment: () => comment() ?? movieQuery.data?.movie.comment,
setComment,
updateMovie,
movieQuery,
}
}

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

View File

@@ -0,0 +1,6 @@
import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'
export default defineConfig({
plugins: [solid()],
})