snapshot of https://github.com/TanStack/query.git (main, 2026-09-16; blobs > 131072B omitted)
25
examples/solid/astro/.gitignore
vendored
Normal 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/
|
||||
47
examples/solid/astro/README.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Astro Starter Kit: Minimal
|
||||
|
||||
```sh
|
||||
npm create astro@latest -- --template minimal
|
||||
```
|
||||
|
||||
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal)
|
||||
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal)
|
||||
[](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).
|
||||
12
examples/solid/astro/astro.config.mjs
Normal 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',
|
||||
}),
|
||||
})
|
||||
25
examples/solid/astro/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
9
examples/solid/astro/public/favicon.svg
Normal 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 |
20
examples/solid/astro/src/components/Link.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
250
examples/solid/astro/src/components/SolidApp.tsx
Normal 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
@@ -0,0 +1,2 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
||||
25
examples/solid/astro/src/layouts/MainLayout.astro
Normal 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>
|
||||
7
examples/solid/astro/src/pages/index.astro
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
import MainLayout from '../layouts/MainLayout.astro';
|
||||
|
||||
const id = Astro.url.searchParams.get('id') || '';
|
||||
---
|
||||
|
||||
<MainLayout id={id} />
|
||||
16
examples/solid/astro/src/utils/index.ts
Normal 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)
|
||||
8
examples/solid/astro/tailwind.config.mjs
Normal 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: [],
|
||||
}
|
||||
7
examples/solid/astro/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js"
|
||||
}
|
||||
}
|
||||
6
examples/solid/basic-graphql-request/.eslintrc.cjs
Normal file
@@ -0,0 +1,6 @@
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
const config = {}
|
||||
|
||||
module.exports = config
|
||||
4
examples/solid/basic-graphql-request/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
.yalc
|
||||
yalc.lock
|
||||
6
examples/solid/basic-graphql-request/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Example
|
||||
|
||||
To run this example:
|
||||
|
||||
- `pnpm install`
|
||||
- `pnpm dev`
|
||||
16
examples/solid/basic-graphql-request/index.html
Normal 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>
|
||||
22
examples/solid/basic-graphql-request/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
examples/solid/basic-graphql-request/src/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
176
examples/solid/basic-graphql-request/src/index.tsx
Normal 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)
|
||||
25
examples/solid/basic-graphql-request/tsconfig.json
Normal 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"]
|
||||
}
|
||||
6
examples/solid/basic-graphql-request/vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import solid from 'vite-plugin-solid'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [solid()],
|
||||
})
|
||||
6
examples/solid/basic/.eslintrc.cjs
Normal 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
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
.yalc
|
||||
yalc.lock
|
||||
6
examples/solid/basic/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Example
|
||||
|
||||
To run this example:
|
||||
|
||||
- `pnpm install`
|
||||
- `pnpm dev`
|
||||
16
examples/solid/basic/index.html
Normal 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>
|
||||
20
examples/solid/basic/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
examples/solid/basic/src/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
157
examples/solid/basic/src/index.tsx
Normal 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)
|
||||
25
examples/solid/basic/tsconfig.json
Normal 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"]
|
||||
}
|
||||
6
examples/solid/basic/vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import solid from 'vite-plugin-solid'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [solid()],
|
||||
})
|
||||
6
examples/solid/default-query-function/.eslintrc.cjs
Normal file
@@ -0,0 +1,6 @@
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
const config = {}
|
||||
|
||||
module.exports = config
|
||||
4
examples/solid/default-query-function/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
.yalc
|
||||
yalc.lock
|
||||
6
examples/solid/default-query-function/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Example
|
||||
|
||||
To run this example:
|
||||
|
||||
- `pnpm install`
|
||||
- `pnpm dev`
|
||||
16
examples/solid/default-query-function/index.html
Normal 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>
|
||||
20
examples/solid/default-query-function/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
examples/solid/default-query-function/src/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
142
examples/solid/default-query-function/src/index.tsx
Normal 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)
|
||||
25
examples/solid/default-query-function/tsconfig.json
Normal 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"]
|
||||
}
|
||||
6
examples/solid/default-query-function/vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import solid from 'vite-plugin-solid'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [solid()],
|
||||
})
|
||||
6
examples/solid/offline/.eslintrc.cjs
Normal file
@@ -0,0 +1,6 @@
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
const config = {}
|
||||
|
||||
module.exports = config
|
||||
6
examples/solid/offline/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Example
|
||||
|
||||
To run this example:
|
||||
|
||||
- `pnpm install`
|
||||
- `pnpm dev`
|
||||
16
examples/solid/offline/index.html
Normal 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>
|
||||
28
examples/solid/offline/package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
208
examples/solid/offline/src/App.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
84
examples/solid/offline/src/api.ts
Normal 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()),
|
||||
)
|
||||
BIN
examples/solid/offline/src/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
18
examples/solid/offline/src/index.tsx
Normal 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,
|
||||
)
|
||||
57
examples/solid/offline/src/movies.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
25
examples/solid/offline/tsconfig.json
Normal 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"]
|
||||
}
|
||||
6
examples/solid/offline/vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import solid from 'vite-plugin-solid'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [solid()],
|
||||
})
|
||||
8
examples/solid/simple/.eslintrc.cjs
Normal 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
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
.yalc
|
||||
yalc.lock
|
||||
6
examples/solid/simple/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Example
|
||||
|
||||
To run this example:
|
||||
|
||||
- `pnpm install`
|
||||
- `pnpm dev`
|
||||
16
examples/solid/simple/index.html
Normal 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>
|
||||
21
examples/solid/simple/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
examples/solid/simple/src/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
55
examples/solid/simple/src/index.tsx
Normal 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)
|
||||
25
examples/solid/simple/tsconfig.json
Normal 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"]
|
||||
}
|
||||
6
examples/solid/simple/vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import solid from 'vite-plugin-solid'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [solid()],
|
||||
})
|
||||
29
examples/solid/solid-start-streaming/.gitignore
vendored
Normal 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
|
||||
32
examples/solid/solid-start-streaming/README.md
Normal 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)
|
||||
3
examples/solid/solid-start-streaming/app.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { defineConfig } from '@solidjs/start/config'
|
||||
|
||||
export default defineConfig({})
|
||||
23
examples/solid/solid-start-streaming/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
examples/solid/solid-start-streaming/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 664 B |
BIN
examples/solid/solid-start-streaming/public/imgs/deferred.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
examples/solid/solid-start-streaming/public/imgs/streaming.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
98
examples/solid/solid-start-streaming/src/app.css
Normal 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;
|
||||
}
|
||||
45
examples/solid/solid-start-streaming/src/app.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
21
examples/solid/solid-start-streaming/src/entry-server.tsx
Normal 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>
|
||||
)}
|
||||
/>
|
||||
))
|
||||
1
examples/solid/solid-start-streaming/src/global.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="@solidjs/start/env" />
|
||||
19
examples/solid/solid-start-streaming/src/routes/[...404].tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
40
examples/solid/solid-start-streaming/src/routes/deferred.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
17
examples/solid/solid-start-streaming/src/routes/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
35
examples/solid/solid-start-streaming/src/routes/mixed.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
41
examples/solid/solid-start-streaming/src/routes/prefetch.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
37
examples/solid/solid-start-streaming/src/routes/streamed.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
63
examples/solid/solid-start-streaming/src/utils/api.ts
Normal 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(),
|
||||
}
|
||||
}
|
||||
19
examples/solid/solid-start-streaming/tsconfig.json
Normal 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/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||