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

25
examples/solid/astro/.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
# build output
dist/
.vercel/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/

View File

@@ -0,0 +1,47 @@
# Astro Starter Kit: Minimal
```sh
npm create astro@latest -- --template minimal
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
├── src/
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

View File

@@ -0,0 +1,12 @@
import { defineConfig } from 'astro/config'
import solidJs from '@astrojs/solid-js'
import tailwind from '@astrojs/tailwind'
import node from '@astrojs/node'
// https://astro.build/config
export default defineConfig({
output: 'server',
integrations: [solidJs(), tailwind()],
adapter: node({
mode: 'standalone',
}),
})

View File

@@ -0,0 +1,25 @@
{
"name": "@tanstack/query-example-solid-astro",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/node": "^9.1.3",
"@astrojs/solid-js": "^5.0.7",
"@astrojs/tailwind": "^6.0.2",
"@astrojs/vercel": "^8.1.3",
"@tanstack/solid-query": "^5.102.8",
"@tanstack/solid-query-devtools": "^5.102.8",
"astro": "^5.5.6",
"solid-js": "^1.9.7",
"tailwindcss": "^3.4.7",
"typescript": "5.8.3"
}
}

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 749 B

View File

@@ -0,0 +1,20 @@
export const Link = (props: {
href: string
children: any
class?: string
}) => {
// Links doing client-side navigation
return (
<a
href={props.href}
onClick={(e) => {
e.preventDefault()
history.pushState({}, '', props.href)
window.dispatchEvent(new PopStateEvent('popstate'))
}}
class={props.class}
>
{props.children}
</a>
)
}

View File

@@ -0,0 +1,250 @@
import {
QueryClient,
QueryClientProvider,
keepPreviousData,
useQuery,
} from '@tanstack/solid-query'
import { SolidQueryDevtools } from '@tanstack/solid-query-devtools'
import {
For,
Show,
Suspense,
createContext,
createSignal,
useContext,
} from 'solid-js'
import { isServer } from 'solid-js/web'
import { getSearchParams, properCase } from '../utils'
import { Link } from './Link'
const PokemonIdContext = createContext<() => string>()
const usePokemonID = () => {
const id = useContext(PokemonIdContext)
if (!id) throw new Error('PokemonIdContext not found')
return id
}
const MAX_POKEMONS = 100
export const SolidApp = (props: { pokemon?: string }) => {
const client = new QueryClient()
const search = getSearchParams(props.pokemon || '')
return (
<QueryClientProvider client={client}>
<SolidQueryDevtools />
<Suspense fallback={'Loading'}>
<PokemonIdContext.Provider value={search}>
<App />
</PokemonIdContext.Provider>
</Suspense>
</QueryClientProvider>
)
}
const App = () => {
return (
<div class="flex flex-1 overflow-auto">
<SideNav />
<PokemonDetails />
</div>
)
}
const PokemonDetails = () => {
const id = usePokemonID()
return (
<div class="flex-1 flex">
<Show when={id()}>
<Suspense fallback={'Loading'}>
<PokemonDex id={id()} />
</Suspense>
</Show>
<Show when={!id()}>
<div class="justify-center items-center flex-1 flex text-2xl font-medium text-zinc-700">
<div class="">Select a pokemon to see its stats</div>
</div>
</Show>
</div>
)
}
const PokemonDex = (props: { id: string }) => {
const pokemon = useQuery(() => ({
queryKey: ['pokemon', props.id],
queryFn: async () => {
const res = await fetch(
`https://pokeapi.co/api/v2/pokemon/${props.id}`,
).then((res) => res.json())
return res
},
placeholderData: keepPreviousData,
}))
const pokemon_stats = useQuery(() => ({
queryKey: ['pokemon', props.id],
queryFn: async () => {
const res = await fetch(
`https://pokeapi.co/api/v2/pokemon/${props.id}`,
).then((res) => res.json())
return res
},
select(data) {
const nameMap = {
hp: 'HP',
attack: 'Attack',
defense: 'Defense',
'special-attack': 'Special Attack',
'special-defense': 'Special Defense',
speed: 'Speed',
}
const stats = data.stats.map((stat: any) => ({
name: nameMap[stat.stat.name as keyof typeof nameMap],
value: stat.base_stat,
}))
return stats as { name: string; value: number }[]
},
placeholderData: keepPreviousData,
reconcile: 'name',
}))
const is_server_rendered = useQuery(() => ({
queryKey: ['is_server_rendered', props.id],
queryFn: () => {
if (isServer) return true
return false
},
placeholderData: keepPreviousData,
}))
return (
<div class="flex flex-col flex-1">
<Show when={pokemon.data}>
<div class="flex justify-center text-3xl font-semibold py-4">
{properCase(pokemon.data.name)}
</div>
<div class="flex justify-center">
<div class="h-64 w-64 rounded-lg relative overflow-hidden border">
<img
class="h-[400%] w-[400%] max-w-[400%] object-contain blur-2xl brightness-125 contrast-150 absolute z-10 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${props.id}.png`}
alt={properCase(pokemon.data?.name || '')}
/>
<img
class="h-full w-full object-contain relative z-20"
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${props.id}.png`}
alt={properCase(pokemon.data?.name || '')}
/>
</div>
</div>
</Show>
<div class="flex justify-center py-4">
<div>
This query was rendered on the{' '}
<Show
when={is_server_rendered.data}
fallback={
<>
<b>client</b>. Reload this page to see the page rendered on the
server.
</>
}
>
<b>server. </b>
Click on another pokemon to see queries run and render on the
client.
</Show>
</div>
</div>
<Show when={pokemon_stats.data}>
<div class="flex justify-center flex-col gap-2">
<For each={pokemon_stats.data}>
{(stat) => (
<div class="flex gap-2 justify-center items-center">
<div class="w-36">{stat.name}</div>
<div class="w-80 h-8 relative bg-zinc-100 rounded overflow-hidden flex justify-center items-center">
<div
class="absolute z-10 h-full top-0 left-0 bg-lime-300"
style={{
width: `${(stat.value / 160) * 100}%`,
transition: 'width 0.5s',
}}
></div>
<div class="relative z-20 text-sm mix-blend-darken font-semibold">
{stat.value}
</div>
</div>
</div>
)}
</For>
</div>
</Show>
</div>
)
}
const SideNav = () => {
const id = usePokemonID()
const pokemonsList = useQuery(() => ({
queryKey: ['pokemons'],
queryFn: async () => {
const res = await fetch(
`https://pokeapi.co/api/v2/pokemon?limit=${MAX_POKEMONS}`,
).then((res) => res.json())
return res as {
results: { name: string; url: string }[]
}
},
select(data) {
return data.results.map((p) => {
const regex = /\/pokemon\/(\d+)\/$/
const match = p.url.match(regex)
const id = match ? match[1] : ''
return {
name: properCase(p.name),
id,
avatar: `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${id}.png`,
}
})
},
placeholderData: keepPreviousData,
reconcile: 'id',
}))
const activeClass = (pokemonID: string) =>
id() === pokemonID ? 'bg-gray-50' : 'hover:bg-gray-50'
return (
<div class="w-72 border-r flex flex-col px-2">
<div class="pt-3 pb-1 text-sm font-semibold">Pokemons</div>
<div class="flex-1 overflow-auto flex flex-col gap-1">
<For each={pokemonsList.data}>
{(pokemon) => (
<Link
class={`flex gap-2 items-center border rounded p-1 ${activeClass(
pokemon.id,
)}`}
href={`./?id=${pokemon.id}`}
>
<span class="flex bg-zinc-100 border relative justify-center items-center overflow-hidden rounded-sm w-9 h-9 flex-shrink-0">
<img
class="w-7 h-7 relative z-20"
src={pokemon.avatar}
alt={pokemon.name}
/>
</span>
<span>{pokemon.name}</span>
</Link>
)}
</For>
</div>
</div>
)
}

2
examples/solid/astro/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />

View File

@@ -0,0 +1,25 @@
---
import { SolidApp } from "../components/SolidApp";
const { id } = Astro.props;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro</title>
</head>
<body class="text-zinc-900">
<div class="flex h-screen flex-col">
<nav class="flex items-center h-12 border-b px-4 flex-shrink-0">
<div class="font-semibold text-lg">
Solid Query + Astro
</div>
</nav>
<SolidApp pokemon={id} client:load />
</div>
</body>
</html>

View File

@@ -0,0 +1,7 @@
---
import MainLayout from '../layouts/MainLayout.astro';
const id = Astro.url.searchParams.get('id') || '';
---
<MainLayout id={id} />

View File

@@ -0,0 +1,16 @@
import { createSignal } from 'solid-js'
export const getSearchParams = (init: string) => {
const [search, setSearch] = createSignal(init)
if (typeof window !== 'undefined') {
window.addEventListener('popstate', () => {
const location = window.location
const params = new URLSearchParams(location.search)
setSearch(params.get('id') || '')
})
}
return search
}
export const properCase = (str: string) =>
str.charAt(0).toUpperCase() + str.slice(1)

View File

@@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
theme: {
extend: {},
},
plugins: [],
}

View File

@@ -0,0 +1,7 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "solid-js"
}
}

View File

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

View File

@@ -0,0 +1,4 @@
node_modules
dist
.yalc
yalc.lock

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,22 @@
{
"name": "@tanstack/query-example-solid-basic-graphql-request",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/solid-query": "^5.102.8",
"@tanstack/solid-query-devtools": "^5.102.8",
"graphql": "^16.9.0",
"graphql-request": "^7.1.2",
"solid-js": "^1.9.7"
},
"devDependencies": {
"typescript": "5.8.3",
"vite": "^6.4.1",
"vite-plugin-solid": "^2.11.6"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,176 @@
/* @refresh reload */
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/solid-query'
import { SolidQueryDevtools } from '@tanstack/solid-query-devtools'
import { For, Match, Switch, createSignal } from 'solid-js'
import { render } from 'solid-js/web'
import { gql, request } from 'graphql-request'
import type { Accessor, Setter } from 'solid-js'
const endpoint = 'https://graphqlzero.almansi.me/api'
const queryClient = new QueryClient()
type Post = {
id: number
title: string
body: string
}
function App() {
const [postId, setPostId] = createSignal(-1)
return (
<QueryClientProvider client={queryClient}>
<SolidQueryDevtools />
<p>
As you visit the posts below, you will notice them in a loading state
the first time you load them. However, after you return to this list and
click on any posts you have already visited again, you will see them
load instantly and background refresh right before your eyes!{' '}
<strong>
(You may need to throttle your network speed to simulate longer
loading sequences)
</strong>
</p>
{postId() > -1 ? (
<Post postId={postId()} setPostId={setPostId} />
) : (
<Posts setPostId={setPostId} />
)}
</QueryClientProvider>
)
}
function createPosts() {
return useQuery(() => ({
queryKey: ['posts'],
queryFn: async () => {
const {
posts: { data },
} = await request<{ posts: { data: Array<Post> } }>(
endpoint,
gql`
query {
posts {
data {
id
title
}
}
}
`,
)
return data
},
}))
}
function Posts(props: { setPostId: Setter<number> }) {
const state = createPosts()
return (
<div>
<h1>Posts</h1>
<div>
<Switch>
<Match when={state.status === 'pending'}>Loading...</Match>
<Match when={state.status === 'error'}>
<span>Error: {(state.error as Error).message}</span>
</Match>
<Match when={state.data !== undefined}>
<>
<div>
<For each={state.data}>
{(post: any) => (
<p>
<a
onClick={() => props.setPostId(post.id)}
href="#"
style={
// We can find the existing query data here to show bold links for
// ones that are cached
queryClient.getQueryData(['post', post.id])
? {
'font-weight': 'bold',
color: 'green',
}
: {}
}
>
{post.title}
</a>
</p>
)}
</For>
</div>
<div>{state.isFetching ? 'Background Updating...' : ' '}</div>
</>
</Match>
</Switch>
</div>
</div>
)
}
function createPost(postId: Accessor<number>) {
return useQuery(() => ({
queryKey: ['post', postId()],
queryFn: async () => {
const { post } = await request<{ post: Post }>(
endpoint,
gql`
query {
post(id: ${postId()}) {
id
title
body
}
}
`,
)
return post
},
enabled: !!postId(),
}))
}
function Post(props: { postId: number; setPostId: Setter<number> }) {
const state = createPost(() => props.postId)
return (
<div>
<div>
<a onClick={() => props.setPostId(-1)} href="#">
Back
</a>
</div>
<Switch>
<Match when={!props.postId || state.status === 'pending'}>
Loading...
</Match>
<Match when={state.status === 'error'}>
<span>Error: {(state.error as Error).message}</span>
</Match>
<Match when={state.data !== undefined}>
<>
<h1>{state.data?.title}</h1>
<div>
<p>{state.data?.body}</p>
</div>
<div>{state.isFetching ? 'Background Updating...' : ' '}</div>
</>
</Match>
</Switch>
</div>
)
}
const root = document.getElementById('root')
if (!root) throw new Error('Missing #root element')
render(() => <App />, root)

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()],
})

View File

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

4
examples/solid/basic/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules
dist
.yalc
yalc.lock

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,20 @@
{
"name": "@tanstack/query-example-solid-basic",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/solid-query": "^5.102.8",
"@tanstack/solid-query-devtools": "^5.102.8",
"solid-js": "^1.9.7"
},
"devDependencies": {
"typescript": "5.8.3",
"vite": "^6.4.1",
"vite-plugin-solid": "^2.11.6"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,157 @@
/* @refresh reload */
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/solid-query'
import { SolidQueryDevtools } from '@tanstack/solid-query-devtools'
import { For, Match, Switch, createSignal } from 'solid-js'
import { render } from 'solid-js/web'
import type { Component, Setter } from 'solid-js'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
})
type Post = {
id: number
title: string
body: string
}
function createPosts() {
return useQuery(() => ({
queryKey: ['posts'],
queryFn: async (): Promise<Array<Post>> => {
const response = await fetch('https://jsonplaceholder.typicode.com/posts')
return await response.json()
},
}))
}
function Posts(props: { setPostId: Setter<number> }) {
const state = createPosts()
return (
<div>
<h1>Posts</h1>
<div>
<Switch>
<Match when={state.status === 'pending'}>Loading...</Match>
<Match when={state.status === 'error'}>
<span>Error: {(state.error as Error).message}</span>
</Match>
<Match when={state.data !== undefined}>
<>
<div>
<For each={state.data}>
{(post) => (
<p>
<a
onClick={() => props.setPostId(post.id)}
href="#"
style={
// We can access the query data here to show bold links for
// ones that are cached
queryClient.getQueryData(['post', post.id])
? {
'font-weight': 'bold',
color: 'green',
}
: {}
}
>
{post.title}
</a>
</p>
)}
</For>
</div>
<div>{state.isFetching ? 'Background Updating...' : ' '}</div>
</>
</Match>
</Switch>
</div>
</div>
)
}
const getPostById = async (id: number): Promise<Post> => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`,
)
return await response.json()
}
function createPost(postId: number) {
return useQuery(() => ({
queryKey: ['post', postId],
queryFn: () => getPostById(postId),
enabled: !!postId,
}))
}
function Post(props: { postId: number; setPostId: Setter<number> }) {
const state = createPost(props.postId)
return (
<div>
<div>
<a onClick={() => props.setPostId(-1)} href="#">
Back
</a>
</div>
<Switch>
<Match when={!props.postId || state.status === 'pending'}>
Loading...
</Match>
<Match when={state.status === 'error'}>
<span>Error: {(state.error as Error).message}</span>
</Match>
<Match when={state.data !== undefined}>
<>
<h1>{state.data?.title}</h1>
<div>
<p>{state.data?.body}</p>
</div>
<div>{state.isFetching ? 'Background Updating...' : ' '}</div>
</>
</Match>
</Switch>
</div>
)
}
const App: Component = () => {
const [postId, setPostId] = createSignal(-1)
return (
<QueryClientProvider client={queryClient}>
<SolidQueryDevtools />
<p>
As you visit the posts below, you will notice them in a loading state
the first time you load them. However, after you return to this list and
click on any posts you have already visited again, you will see them
load instantly and background refresh right before your eyes!{' '}
<strong>
(You may need to throttle your network speed to simulate longer
loading sequences)
</strong>
</p>
{postId() > -1 ? (
<Post postId={postId()} setPostId={setPostId} />
) : (
<Posts setPostId={setPostId} />
)}
</QueryClientProvider>
)
}
const root = document.getElementById('root')
if (!root) throw new Error('Missing #root element')
render(() => <App />, root)

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()],
})

View File

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

View File

@@ -0,0 +1,4 @@
node_modules
dist
.yalc
yalc.lock

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,20 @@
{
"name": "@tanstack/query-example-solid-default-query-function",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/solid-query": "^5.102.8",
"@tanstack/solid-query-devtools": "^5.102.8",
"solid-js": "^1.9.7"
},
"devDependencies": {
"typescript": "5.8.3",
"vite": "^6.4.1",
"vite-plugin-solid": "^2.11.6"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,142 @@
/* @refresh reload */
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/solid-query'
import { SolidQueryDevtools } from '@tanstack/solid-query-devtools'
import { For, Match, Show, Switch, createSignal } from 'solid-js'
import { render } from 'solid-js/web'
import type { Setter } from 'solid-js'
import type { QueryFunction } from '@tanstack/solid-query'
// Define a default query function that will receive the query key
const defaultQueryFn: QueryFunction<unknown> = async ({ queryKey }) => {
const response = await fetch(
`https://jsonplaceholder.typicode.com${queryKey[0]}`,
{
method: 'GET',
},
)
return response.json()
}
// provide the default query function to your app via the query client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryFn: defaultQueryFn,
},
},
})
function App() {
const [postId, setPostId] = createSignal(-1)
return (
<QueryClientProvider client={queryClient}>
<SolidQueryDevtools />
<p>
As you visit the posts below, you will notice them in a loading state
the first time you load them. However, after you return to this list and
click on any posts you have already visited again, you will see them
load instantly and background refresh right before your eyes!{' '}
<strong>
(You may need to throttle your network speed to simulate longer
loading sequences)
</strong>
</p>
<Show when={postId() > -1} fallback={<Posts setPostId={setPostId} />}>
<Post postId={postId()} setPostId={setPostId} />
</Show>
</QueryClientProvider>
)
}
function Posts(props: { setPostId: Setter<number> }) {
// All you have to do now is pass a key!
const state = useQuery<any[]>(() => ({ queryKey: ['/posts'] }))
return (
<div>
<h1>Posts</h1>
<div>
<Switch>
<Match when={state.status === 'pending'}>Loading...</Match>
<Match when={state.status === 'error'}>
<span>Error: {(state.error as Error).message}</span>
</Match>
<Match when={state.data !== undefined}>
<>
<div>
<For each={state.data}>
{(post) => (
<p>
<a
onClick={() => props.setPostId(post.id)}
href="#"
style={
// We can use the queryCache here to show bold links for
// ones that are cached
queryClient.getQueryData(['post', post.id])
? {
'font-weight': 'bold',
color: 'green',
}
: {}
}
>
{post.title}
</a>
</p>
)}
</For>
</div>
<div>{state.isFetching ? 'Background Updating...' : ' '}</div>
</>
</Match>
</Switch>
</div>
</div>
)
}
function Post(props: { postId: number; setPostId: Setter<number> }) {
// You can even leave out the queryFn and just go straight into options
const state = useQuery<any>(() => ({
queryKey: [`/posts/${props.postId}`],
enabled: !!props.postId,
}))
return (
<div>
<div>
<a onClick={() => props.setPostId(-1)} href="#">
Back
</a>
</div>
<Switch>
<Match when={!props.postId || state.status === 'pending'}>
Loading...
</Match>
<Match when={state.status === 'error'}>
<span>Error: {(state.error as Error).message}</span>
</Match>
<Match when={state.data !== undefined}>
<>
<h1>{state.data.title}</h1>
<div>
<p>{state.data.body}</p>
</div>
<div>{state.isFetching ? 'Background Updating...' : ' '}</div>
</>
</Match>
</Switch>
</div>
)
}
const root = document.getElementById('root')
if (!root) throw new Error('Missing #root element')
render(() => <App />, root)

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()],
})

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()],
})

View File

@@ -0,0 +1,8 @@
// @ts-check
/** @type {import('eslint').Linter.Config} */
const config = {
extends: ['plugin:@tanstack/eslint-plugin-query/recommended'],
}
module.exports = config

4
examples/solid/simple/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules
dist
.yalc
yalc.lock

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,21 @@
{
"name": "@tanstack/query-example-solid-simple",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/solid-query": "^5.102.8",
"@tanstack/solid-query-devtools": "^5.102.8",
"solid-js": "^1.9.7"
},
"devDependencies": {
"@tanstack/eslint-plugin-query": "^5.102.8",
"typescript": "5.8.3",
"vite": "^6.4.1",
"vite-plugin-solid": "^2.11.6"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,55 @@
/* @refresh reload */
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/solid-query'
import { SolidQueryDevtools } from '@tanstack/solid-query-devtools'
import { Match, Switch } from 'solid-js'
import { render } from 'solid-js/web'
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<SolidQueryDevtools />
<Example />
</QueryClientProvider>
)
}
function Example() {
const state = useQuery(() => ({
queryKey: ['repoData'],
queryFn: async () => {
const response = await fetch(
'https://api.github.com/repos/TanStack/query',
)
return await response.json()
},
}))
return (
<Switch>
<Match when={state.isPending}>Loading...</Match>
<Match when={state.error}>
{'An error has occurred: ' + (state.error as Error).message}
</Match>
<Match when={state.data !== undefined}>
<div>
<h1>{state.data.name}</h1>
<p>{state.data.description}</p>
<strong>👀 {state.data.subscribers_count}</strong>{' '}
<strong>✨ {state.data.stargazers_count}</strong>{' '}
<strong>🍴 {state.data.forks_count}</strong>
<div>{state.isFetching ? 'Updating...' : ''}</div>
</div>
</Match>
</Switch>
)
}
const root = document.getElementById('root')
if (!root) throw new Error('Missing #root element')
render(() => <App />, root)

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", ".eslintrc.cjs", "vite.config.ts"]
}

View File

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

View File

@@ -0,0 +1,29 @@
dist
.solid
.output
.vercel
.netlify
netlify
.vinxi
# Environment
.env
.env*.local
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
*.launch
.settings/
# Temp
gitignore
# System Files
.DS_Store
Thumbs.db

View File

@@ -0,0 +1,32 @@
# SolidStart
Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com);
## Creating a project
```bash
# create a new project in the current directory
npm init solid@latest
# create a new project in my-app
npm init solid@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
Solid apps are built with _presets_, which optimise your project for deployment to different environments.
By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`.
## This project was created with the [Solid CLI](https://solid-cli.netlify.app)

View File

@@ -0,0 +1,3 @@
import { defineConfig } from '@solidjs/start/config'
export default defineConfig({})

View File

@@ -0,0 +1,23 @@
{
"name": "@tanstack/query-example-solid-start-streaming",
"private": true,
"type": "module",
"scripts": {
"dev": "vinxi dev",
"build": "vinxi build",
"start": "vinxi start",
"version": "vinxi version"
},
"dependencies": {
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.3",
"@solidjs/start": "^1.1.3",
"@tanstack/solid-query": "^5.102.8",
"@tanstack/solid-query-devtools": "^5.102.8",
"solid-js": "^1.9.7",
"vinxi": "^0.5.3"
},
"engines": {
"node": ">=18"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -0,0 +1,98 @@
body {
font-family:
Gordita, Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue',
sans-serif;
line-height: 1.5;
}
a {
margin-right: 1rem;
}
main {
text-align: center;
padding: 1em;
margin: 0 auto;
}
h1 {
color: #335d92;
text-transform: uppercase;
font-size: 2.5rem;
font-weight: 100;
line-height: 1.1;
margin: 2.5rem auto 0 auto;
max-width: 14rem;
}
p {
max-width: 14rem;
margin: 1rem auto;
line-height: 1.35;
}
@media (min-width: 480px) {
h1 {
max-width: none;
}
p {
max-width: none;
}
}
.loader {
color: #888;
margin-top: 1rem;
}
.error {
color: red;
margin-top: 1rem;
}
.description {
max-width: 60rem;
margin: 3rem auto;
}
.description p {
line-height: 1.5;
}
.description *:not(:first-child) {
margin-top: 1rem;
}
.description_img {
max-width: 100%;
border-radius: 4px;
}
.example {
border: 1px solid #ddd;
padding: 1rem;
max-width: 50rem;
text-align: left;
margin: 1rem auto;
}
.example__header {
display: flex;
}
.example__title {
font-size: 1rem;
font-weight: bold;
flex-grow: 1;
}
.example--table {
padding: 0.5rem;
}
.example--table th,
.example--table td {
padding: 4px 12px;
white-space: nowrap;
}

View File

@@ -0,0 +1,45 @@
// @refresh reload
import { MetaProvider, Title } from '@solidjs/meta'
import { A, Router } from '@solidjs/router'
import { FileRoutes } from '@solidjs/start/router'
import { Suspense } from 'solid-js'
import { SolidQueryDevtools } from '@tanstack/solid-query-devtools'
import { QueryClient, QueryClientProvider } from '@tanstack/solid-query'
import './app.css'
export default function App() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
staleTime: 5000,
},
},
})
return (
<QueryClientProvider client={queryClient}>
<SolidQueryDevtools />
<Router
root={(props) => (
<MetaProvider>
<Title>SolidStart - Basic</Title>
<a href="/">Home</a>
<a href="/streamed">Streamed</a>
<a href="/deferred">Deferred</a>
<a href="/mixed">Mixed</a>
<a href="/with-error">With Error</a>
<a href="/hydration">Hydration</a>
<A preload={true} href="/prefetch">
Prefetch
</A>
<a href="/batch-methods">Batching Methods</a>
<Suspense>{props.children}</Suspense>
</MetaProvider>
)}
>
<FileRoutes />
</Router>
</QueryClientProvider>
)
}

View File

@@ -0,0 +1,23 @@
import type { ParentComponent } from 'solid-js'
export interface ExampleProps {
title: string
deferStream?: boolean
sleep?: number
}
export const Example: ParentComponent<ExampleProps> = (props) => {
return (
<div class="example">
<div class="example__header">
<div class="example__title">{props.title}</div>
<div>[deferStream={String(props.deferStream || false)}]</div>
<div style={{ 'margin-left': '10px' }}>
[simulated sleep: {props.sleep || 0}ms]
</div>
</div>
{props.children}
</div>
)
}

View File

@@ -0,0 +1,93 @@
import { useQuery } from '@tanstack/solid-query'
import { resetErrorBoundaries } from 'solid-js'
import { createSignal } from 'solid-js'
import { For } from 'solid-js'
import { Example } from './example'
import { QueryBoundary } from './query-boundary'
import type { Component } from 'solid-js'
import { fetchPost } from '~/utils/api'
export interface PostViewerProps {
deferStream?: boolean
sleep?: number
simulateError?: boolean
}
export const PostViewer: Component<PostViewerProps> = (props) => {
const [simulateError, setSimulateError] = createSignal(props.simulateError)
const [postId, setPostId] = createSignal(1)
const query = useQuery(() => ({
queryKey: ['posts', postId()],
queryFn: () =>
fetchPost({
postId: postId(),
sleep: props.sleep,
simulateError:
simulateError() || (simulateError() !== false && postId() === 5),
}),
deferStream: props.deferStream,
throwOnError: true,
}))
return (
<Example
title="Post Query"
deferStream={props.deferStream}
sleep={props.sleep}
>
<div style={{ 'margin-top': '20px' }}>
<button
onClick={() => {
setPostId(Math.max(postId() - 1, 1))
resetErrorBoundaries()
}}
>
Previous Post
</button>
<button
onClick={() => {
setPostId(Math.min(postId() + 1, 100))
resetErrorBoundaries()
}}
>
Next Post
</button>
</div>
{/* NOTE: without this extra wrapping div, for some reason solid ends up printing two errors... feels like a bug in solid. */}
<div>
<QueryBoundary
query={query}
loadingFallback={<div class="loader">loading post...</div>}
errorFallback={(err, retry) => (
<div>
<div class="error">{err.message}</div>
<button
onClick={() => {
setSimulateError(false)
retry()
}}
>
retry
</button>
</div>
)}
>
{(posts) => (
<For each={posts}>
{(post) => (
<div style={{ 'margin-top': '20px' }}>
<b>
[post {postId()}] {post.title}
</b>
<p>{post.body}</p>
</div>
)}
</For>
)}
</QueryBoundary>
</div>
</Example>
)
}

View File

@@ -0,0 +1,93 @@
import { ErrorBoundary, Match, Suspense, Switch, children } from 'solid-js'
import type { UseQueryResult } from '@tanstack/solid-query'
import type { JSX } from 'solid-js'
export interface QueryBoundaryProps<T = unknown> {
query: UseQueryResult<T, Error>
/**
* Triggered when the data is initially loading.
*/
loadingFallback?: JSX.Element
/**
* Triggered when fetching is complete, but the returned data was falsey.
*/
notFoundFallback?: JSX.Element
/**
* Triggered when the query results in an error.
*/
errorFallback?: (err: Error, retry: () => void) => JSX.Element
/**
* Triggered when fetching is complete, and the returned data is not falsey.
*/
children: (data: Exclude<T, null | false | undefined>) => JSX.Element
}
/**
* Convenience wrapper that handles suspense and errors for queries. Makes the results of query.data available to
* children (as a render prop) in a type-safe way.
*/
export function QueryBoundary<T>(props: QueryBoundaryProps<T>) {
return (
<Suspense fallback={props.loadingFallback}>
<ErrorBoundary
fallback={(err: Error, reset) =>
props.errorFallback ? (
props.errorFallback(err, async () => {
await props.query.refetch()
reset()
})
) : (
<div>
<div class="error">{err.message}</div>
<button
onClick={async () => {
await props.query.refetch()
reset()
}}
>
retry
</button>
</div>
)
}
>
<Switch>
{/* <Match when={props.query.isError}>
{props.errorFallback ? (
props.errorFallback
) : (
<div>
<div class="error">{props.query.error?.message}</div>
<button
onClick={() => {
props.query.refetch();
}}
>
retry
</button>
</div>
)}
</Match> */}
<Match when={!props.query.isFetching && !props.query.data}>
{props.notFoundFallback ? (
props.notFoundFallback
) : (
<div>not found</div>
)}
</Match>
<Match when={props.query.data}>
{props.children(
props.query.data as Exclude<T, null | false | undefined>,
)}
</Match>
</Switch>
</ErrorBoundary>
</Suspense>
)
}

View File

@@ -0,0 +1,72 @@
import { useQuery } from '@tanstack/solid-query'
import { createSignal } from 'solid-js'
import { Example } from './example'
import { QueryBoundary } from './query-boundary'
import type { Component } from 'solid-js'
import { fetchUser } from '~/utils/api'
export interface UserInfoProps {
deferStream?: boolean
sleep?: number
simulateError?: boolean
staleTime?: number
gcTime?: number
}
export const userInfoQueryOpts = (props?: UserInfoProps) => ({
queryKey: ['user'],
queryFn: () => fetchUser(props),
deferStream: props?.deferStream,
staleTime: props?.staleTime,
gcTime: props?.gcTime,
throwOnError: true,
})
export const UserInfo: Component<UserInfoProps> = (props) => {
const [simulateError, setSimulateError] = createSignal(props.simulateError)
const query = useQuery(() =>
userInfoQueryOpts({ ...props, simulateError: simulateError() }),
)
return (
<Example
title="User Query"
deferStream={props.deferStream}
sleep={props.sleep}
>
<QueryBoundary
query={query}
loadingFallback={<div class="loader">loading user...</div>}
errorFallback={(err, retry) => (
<div>
<div class="error">{err.message}</div>
<button
onClick={() => {
setSimulateError(false)
retry()
}}
>
retry
</button>
</div>
)}
>
{(user) => (
<>
<div>id: {user.id}</div>
<div>name: {user.name}</div>
<div>queryTime: {user.queryTime}</div>
<button
onClick={() => {
query.refetch()
}}
>
refetch
</button>
</>
)}
</QueryBoundary>
</Example>
)
}

View File

@@ -0,0 +1,7 @@
// @refresh reload
import { mount, StartClient } from '@solidjs/start/client'
const app = document.getElementById('app')
if (!app) throw new Error('Missing #app element')
mount(() => <StartClient />, app)

View File

@@ -0,0 +1,21 @@
// @refresh reload
import { createHandler, StartServer } from '@solidjs/start/server'
export default createHandler(() => (
<StartServer
document={({ assets, children, scripts }) => (
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
{assets}
</head>
<body>
<div id="app">{children}</div>
{scripts}
</body>
</html>
)}
/>
))

View File

@@ -0,0 +1 @@
/// <reference types="@solidjs/start/env" />

View File

@@ -0,0 +1,19 @@
import { Title } from '@solidjs/meta'
import { HttpStatusCode } from '@solidjs/start'
export default function NotFound() {
return (
<main>
<Title>Not Found</Title>
<HttpStatusCode code={404} />
<h1>Page Not Found</h1>
<p>
Visit{' '}
<a href="https://start.solidjs.com" target="_blank">
start.solidjs.com
</a>{' '}
to learn how to build SolidStart apps.
</p>
</main>
)
}

View File

@@ -0,0 +1,76 @@
import { notifyManager, useQuery } from '@tanstack/solid-query'
import { createSignal } from 'solid-js'
import { QueryBoundary } from '~/components/query-boundary'
function sleep(milliseconds: number) {
return new Promise((res) => setTimeout(res, milliseconds))
}
function spin(milliseconds: number) {
const start = performance.now()
while (performance.now() - start <= milliseconds) {
// do nothing
}
}
async function sayHello(name: string) {
console.info('[api] sayHello.start')
await sleep(500)
// make the layout shift more obvious, it doesn't always happen
console.time('[api] sayHello.spin')
spin(20)
console.timeEnd('[api] sayHello.spin')
console.info('[api] sayHello.end')
return `Hello ${name}`
}
export default function BatchMethods() {
const [count, setCount] = createSignal(0)
const hello = useQuery(() => ({
queryKey: ['hello', count()] as const,
queryFn: ({ queryKey: [_, count] }) => sayHello(`solid ${count}`),
}))
return (
<div>
<select
value="timer"
ref={(el) => (el.value = 'timer')} // browser caches form input
onInput={(e) => {
const type = e.currentTarget.value
if (type === 'raf') notifyManager.setScheduler(requestAnimationFrame)
if (type === 'tick') notifyManager.setScheduler(queueMicrotask)
if (type === 'timer')
notifyManager.setScheduler((cb) => setTimeout(cb, 0))
}}
>
<option value="raf">requestAnimationFrame</option>
<option value="timer">setTimeout</option>
<option value="tick">queueMicrotick</option>
</select>
<button class="increment" onClick={() => setCount((x) => x + 1)}>
Clicks: {count()}
</button>
<p>
<QueryBoundary loadingFallback={'Loading...'} query={hello}>
{(data) => <div style={{ 'background-color': 'aqua' }}>{data}</div>}
</QueryBoundary>
</p>
<div style={{ 'background-color': 'red' }}>
Something below to demonstrate layout shift
</div>
<p>
Due to the way solidjs handles updates, sometimes the updating of a
query results in DOM modifications triggering a rerender twice. This is
perceived as a glitch in the layout of the webpage that usually lasts
for one frame. By using another batching strategy in the browser,
instead of the default setTimeout one, we can mitigate this issue. Try
out requestAnimationFrame or queueMicrotick.
</p>
</div>
)
}

View File

@@ -0,0 +1,40 @@
import { Title } from '@solidjs/meta'
import { PostViewer } from '~/components/post-viewer'
import { UserInfo } from '~/components/user-info'
export default function Deferred() {
return (
<main>
<Title>Solid Query - Deferred</Title>
<h1>Solid Query - Deferred Example</h1>
<div class="description">
<p>
Both queries are configured with deferStream=true, so the server will
not start streaming HTML to the client until the queries have
resolved. In this case we are not really taking advantage of streaming
- this mimics traditional renderAsync + Suspense behavior. Note how
the green bar in the devtools is much larger now - the client does not
start receiving any information until 2+ seconds (both queries
resolve).
</p>
<p>
Clients with javascript disabled will see the resolved state for both
queries. (try turning off javascript and reloading this page)
</p>
<img
class="description_img"
src="/imgs/deferred.png"
alt="devtools deferred requests"
/>
</div>
<UserInfo sleep={2000} deferStream />
<PostViewer sleep={2000} deferStream />
</main>
)
}

View File

@@ -0,0 +1,99 @@
import { useQuery } from '@tanstack/solid-query'
import { Suspense, createSignal } from 'solid-js'
import { NoHydration } from 'solid-js/web'
import { Title } from '@solidjs/meta'
import type { UseQueryResult } from '@tanstack/solid-query'
import { fetchUser } from '~/utils/api'
export default function Hydration() {
const query = useQuery(() => ({
queryKey: ['user'],
queryFn: () => fetchUser({ sleep: 500 }),
deferStream: true,
}))
const [initialQueryState] = createSignal(JSON.parse(JSON.stringify(query)))
return (
<main>
<Title>Solid Query - Hydration</Title>
<h1>Solid Query - Hydration Example</h1>
<div class="description">
<p>
Lists the query state as seen on the server, initial render on the
client (right after hydration), and current client value. Ideally, if
SSR is setup correctly, these values are exactly the same in all three
contexts.
</p>
</div>
<button onClick={() => query.refetch()}>Refetch</button>
<table class="example example--table">
<thead>
<tr>
<th>Context</th>
<th>data.name</th>
<th>isFetching</th>
<th>isFetched</th>
<th>isPending</th>
<th>isRefetching</th>
<th>isLoading</th>
<th>isStale</th>
<th>isSuccess</th>
<th>isError</th>
<th>error</th>
<th>fetchStatus</th>
<th>dataUpdatedAt</th>
</tr>
</thead>
<tbody>
<Suspense>
<NoHydration>
<QueryStateRow context="server" query={query} />
</NoHydration>
<QueryStateRow
context="client (initial render)"
query={initialQueryState()!}
/>
<QueryStateRow context="client" query={query} />
</Suspense>
</tbody>
</table>
</main>
)
}
type QueryState = UseQueryResult<
{
id: string
name: string
queryTime: number
},
Error
>
const QueryStateRow = (props: { context: string; query: QueryState }) => {
return (
<tr>
<td>{props.context}</td>
<td>{props.query.data?.name}</td>
<td>{String(props.query.isFetching)}</td>
<td>{String(props.query.isFetched)}</td>
<td>{String(props.query.isPending)}</td>
<td>{String(props.query.isRefetching)}</td>
<td>{String(props.query.isLoading)}</td>
<td>{String(props.query.isStale)}</td>
<td>{String(props.query.isSuccess)}</td>
<td>{String(props.query.isError)}</td>
<td>{String(props.query.error)}</td>
<td>{String(props.query.fetchStatus)}</td>
<td>{String(props.query.dataUpdatedAt)}</td>
</tr>
)
}

View File

@@ -0,0 +1,17 @@
import { Title } from '@solidjs/meta'
export default function Home() {
return (
<main>
<Title>Solid Query v5</Title>
<h1>Solid Query v5</h1>
<p>
This demo demonstrates how Solid Query can be used in SSR, with
streaming support. Use the links in the top left to navigate between the
various examples.
</p>
</main>
)
}

View File

@@ -0,0 +1,35 @@
import { Title } from '@solidjs/meta'
import { PostViewer } from '~/components/post-viewer'
import { UserInfo } from '~/components/user-info'
export default function Mixed() {
return (
<main>
<Title>Solid Query - Mixed</Title>
<h1>Solid Query - Mixed Example</h1>
<div class="description">
<p>
You may want to require that key queries resolve before the initial
HTML is streamed to the client, while allowing the rest of your
queries to stream to the client as they resolve. A common use case for
this is to populate SEO meta tags and/or social graph information for
unfurl scenarios such as sharing the page as a link in Slack.
</p>
<p>
In this example, the quick (100ms) user query has deferStream set to
true, while the more expensive post query (1000ms) is streamed to the
client when ready. Clients with javascript disabled will see the
resolved state for the user query, and the loading state for the post
query. (try turning off javascript and reloading this page)
</p>
</div>
<UserInfo deferStream sleep={100} />
<PostViewer sleep={1000} />
</main>
)
}

View File

@@ -0,0 +1,41 @@
import { noop, useQueryClient } from '@tanstack/solid-query'
import { isServer } from 'solid-js/web'
import { Title } from '@solidjs/meta'
import { UserInfo, userInfoQueryOpts } from '~/components/user-info'
export const route = {
load: () => {
const queryClient = useQueryClient()
// Prefetching the user info and caching it for 15 seconds.
queryClient
.query(userInfoQueryOpts({ sleep: 500, gcTime: 15000 }))
.catch(noop)
},
}
export default function Prefetch() {
return (
<main>
<Title>Solid Query - Prefetch</Title>
<h1>Solid Query - Prefetch Example</h1>
<div class="description">
<p>
SolidStart now supports link prefetching. This means that when a user
hovers over a link, the browser can prefetch the data for that page
before the user even clicks on the link. This can make navigating
around your app feel much faster.
</p>
<p>
To see this in action, go to the home page and reload the page. Then
hover over the "Prefetch" link in the navigation. You should see the
user data prefetch in the background and in the devtools. When you
click on the link, the page should load instantly.
</p>
</div>
<UserInfo sleep={500} />
</main>
)
}

View File

@@ -0,0 +1,37 @@
import { Title } from '@solidjs/meta'
import { PostViewer } from '~/components/post-viewer'
import { UserInfo } from '~/components/user-info'
export default function Streamed() {
return (
<main>
<Title>Solid Query - Streamed</Title>
<h1>Solid Query - Streamed Example</h1>
<div class="description">
<p>
HTML is streamed from the server ASAP, reducing key metrics such as
TTFB and TTI. Suspended queries are streamed to the client when they
resolve on the server. This is represented in your devtools by the
green and blue chunks of the waterfall.
</p>
<p>
Clients with javascript disabled will see the loading state for both
queries. (try turning off javascript and reloading this page)
</p>
<img
class="description_img"
src="/imgs/streaming.png"
alt="devtools streaming request"
/>
</div>
<UserInfo sleep={2500} />
<PostViewer sleep={300} />
</main>
)
}

View File

@@ -0,0 +1,25 @@
import { Title } from '@solidjs/meta'
import { PostViewer } from '~/components/post-viewer'
import { UserInfo } from '~/components/user-info'
export default function Streamed() {
return (
<main>
<Title>Solid Query - Errors</Title>
<h1>Solid Query - Errors</h1>
<div class="description">
<p>
For more control over error handling, try leveraging the `Switch`
component and watching the reactive `query.isError` property. See
`components/query-boundary.tsx` for one possible approach.
</p>
</div>
<UserInfo sleep={10} deferStream simulateError />
<PostViewer sleep={3000} deferStream simulateError />
</main>
)
}

View File

@@ -0,0 +1,63 @@
interface PostData {
userId: number
id: number
title: string
body: string
}
const doSleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
export const fetchPost = async ({
postId,
simulateError,
sleep,
}: {
postId: number
simulateError?: boolean
sleep?: number
}) => {
console.info('[api] fetchPost.start', { postId, sleep, simulateError })
let response
if (!simulateError) {
response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${postId}`,
).then((res) => res.json())
}
// simulate extra latency to make things like streaming behavior more clear
if (sleep) {
await doSleep(sleep)
}
console.info('[api] fetchPost.done', { postId, sleep, simulateError })
if (simulateError) {
throw new Error('API request to get post was not OK')
}
return [response] as PostData[]
}
export const fetchUser = async ({
sleep,
simulateError,
}: { sleep?: number; simulateError?: boolean } = {}) => {
console.info('[api] fetchUser.start', { sleep, simulateError })
if (sleep) {
await doSleep(sleep)
}
console.info('[api] fetchUser.done', { sleep, simulateError })
if (simulateError) {
throw new Error('API request to get user was not OK')
}
return {
id: 'abc',
name: `john doe`,
queryTime: Date.now(),
}
}

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"allowJs": true,
"strict": true,
"noEmit": true,
"types": ["vinxi/client", "vite/client"],
"isolatedModules": true,
"paths": {
"~/*": ["./src/*"]
}
}
}