Skip to content

Commit 7ef04e5

Browse files
committed
feat: add useSuspendAll hook & react/suspense example
1 parent f098c92 commit 7ef04e5

25 files changed

+1285
-20
lines changed

.codesandbox/ci.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"github/reduxjs/rtk-github-issues-example",
66
"/examples/query/react/basic",
77
"/examples/query/react/advanced",
8-
"/examples/action-listener/counter"
8+
"/examples/action-listener/counter",
9+
"/examples/query/react/suspense"
910
],
1011
"node": "14",
1112
"buildCommand": "build:packages",

examples/query/react/suspense/.env

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SKIP_PREFLIGHT_CHECK=true
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"name": "@examples-query-react/suspense",
3+
"private": true,
4+
"version": "1.0.0",
5+
"description": "",
6+
"keywords": [],
7+
"main": "src/index.tsx",
8+
"dependencies": {
9+
"@reduxjs/toolkit": "^1.8.0",
10+
"clsx": "^1.1.1",
11+
"react": "17.0.0",
12+
"react-dom": "17.0.0",
13+
"react-error-boundary": "3.1.4",
14+
"react-redux": "7.2.2",
15+
"react-scripts": "4.0.2",
16+
"use-sync-external-store": "^1.0.0"
17+
},
18+
"devDependencies": {
19+
"@types/react": "17.0.0",
20+
"@types/react-dom": "17.0.0",
21+
"@types/react-redux": "7.1.9",
22+
"@types/use-sync-external-store": "^0.0.3",
23+
"typescript": "~4.2.4"
24+
},
25+
"eslintConfig": {
26+
"extends": [
27+
"react-app"
28+
],
29+
"rules": {
30+
"react/react-in-jsx-scope": "off"
31+
}
32+
},
33+
"scripts": {
34+
"start": "react-scripts start",
35+
"build": "react-scripts build"
36+
},
37+
"browserslist": [
38+
">0.2%",
39+
"not dead",
40+
"not ie <= 11",
41+
"not op_mini all"
42+
]
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta
6+
name="viewport"
7+
content="width=device-width, initial-scale=1, shrink-to-fit=no"
8+
/>
9+
<meta name="theme-color" content="#000000" />
10+
<!--
11+
manifest.json provides metadata used when your web app is added to the
12+
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
13+
-->
14+
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
15+
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
16+
<!--
17+
Notice the use of %PUBLIC_URL% in the tags above.
18+
It will be replaced with the URL of the `public` folder during the build.
19+
Only files inside the `public` folder can be referenced from the HTML.
20+
21+
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
22+
work correctly both with client-side routing and a non-root public URL.
23+
Learn how to configure a non-root public URL by running `npm run build`.
24+
-->
25+
<title>React App</title>
26+
</head>
27+
28+
<body>
29+
<noscript> You need to enable JavaScript to run this app. </noscript>
30+
<div id="root"></div>
31+
<!--
32+
This HTML file is a template.
33+
If you open it directly in the browser, you will see an empty page.
34+
35+
You can add webfonts, meta tags, or analytics to this file.
36+
The build step will place the bundled scripts into the <body> tag.
37+
38+
To begin the development, run `npm start` or `yarn start`.
39+
To create a production bundle, use `npm run build` or `yarn build`.
40+
-->
41+
</body>
42+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"short_name": "RTK Query Polling Example",
3+
"name": "Polling Example",
4+
"start_url": ".",
5+
"display": "standalone",
6+
"theme_color": "#000000",
7+
"background_color": "#ffffff"
8+
}
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import * as React from 'react'
2+
import { POKEMON_NAMES } from './pokemon.data'
3+
import './styles.css'
4+
import { PokemonSingleQueries } from './PokemonSingleQueries'
5+
import { PokemonParallelQueries } from './PokemonParallelQueries'
6+
7+
const getRandomPokemonName = () =>
8+
POKEMON_NAMES[Math.floor(Math.random() * POKEMON_NAMES.length)]
9+
10+
export default function App() {
11+
const [errorRate, setErrorRate] = React.useState<number>(
12+
window.fetchFnErrorRate
13+
)
14+
15+
React.useEffect(() => {
16+
window.fetchFnErrorRate = errorRate
17+
}, [errorRate])
18+
19+
return (
20+
<div className="App">
21+
<div>
22+
<form action="#" className="global-controls">
23+
<label htmlFor="error-rate-input">
24+
fetch error rate: {errorRate}
25+
<input
26+
type="range"
27+
name="erro-rate"
28+
id="error-rate-input"
29+
min="0"
30+
max="1"
31+
step="0.1"
32+
value={errorRate}
33+
onChange={(evt) => {
34+
setErrorRate(Number(evt.currentTarget.value))
35+
}}
36+
/>
37+
</label>
38+
</form>
39+
</div>
40+
<PokemonParallelQueries />
41+
<hr />
42+
<PokemonSingleQueries />
43+
</div>
44+
)
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import * as React from 'react'
2+
import { useSuspendAll } from '@reduxjs/toolkit/query/react'
3+
import { useGetPokemonByNameQuery } from './services/pokemon'
4+
import type { PokemonName } from './pokemon.data'
5+
6+
const intervalOptions = [
7+
{ label: 'Off', value: 0 },
8+
{ label: '20s', value: 10000 },
9+
{ label: '1m', value: 60000 },
10+
]
11+
12+
const getRandomIntervalValue = () =>
13+
intervalOptions[Math.floor(Math.random() * intervalOptions.length)].value
14+
15+
export interface PokemonProps {
16+
name: PokemonName
17+
}
18+
19+
export function Pokemon({ name }: PokemonProps) {
20+
const [pollingInterval, setPollingInterval] = React.useState(
21+
getRandomIntervalValue()
22+
)
23+
24+
const [{ data, isFetching, refetch }] = useSuspendAll(
25+
useGetPokemonByNameQuery(name)
26+
)
27+
28+
return (
29+
<section className="pokemon-card">
30+
<h3>{data.species.name}</h3>
31+
<img
32+
src={data.sprites.front_shiny}
33+
alt={data.species.name}
34+
className={'pokemon-card__pic'}
35+
style={{ ...(isFetching ? { opacity: 0.3 } : {}) }}
36+
/>
37+
<div>
38+
<label style={{ display: 'block' }}>Polling interval</label>
39+
<select
40+
value={pollingInterval}
41+
onChange={({ target: { value } }) =>
42+
setPollingInterval(Number(value))
43+
}
44+
>
45+
{intervalOptions.map(({ label, value }) => (
46+
<option key={value} value={value}>
47+
{label}
48+
</option>
49+
))}
50+
</select>
51+
</div>
52+
<div>
53+
<button
54+
type="button"
55+
className={'btn'}
56+
onClick={refetch}
57+
disabled={isFetching}
58+
>
59+
{isFetching ? 'Loading' : 'Manually refetch'}
60+
</button>
61+
</div>
62+
</section>
63+
)
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import * as React from 'react'
2+
import { ErrorBoundary } from 'react-error-boundary'
3+
import { pokemonEvolutions } from './pokemon.data'
4+
import { PokemonPlaceholder } from './PokemonPlaceholder'
5+
import { PokemonWithEvolution } from './PokemonWithEvolution'
6+
7+
const evolutionsKeys = Object.keys(
8+
pokemonEvolutions
9+
) as (keyof typeof pokemonEvolutions)[]
10+
11+
export const PokemonParallelQueries = React.memo(
12+
function PokemonParallelQueries() {
13+
const [evolutions, setEvolutions] = React.useState([
14+
'bulbasaur' as keyof typeof pokemonEvolutions,
15+
])
16+
17+
return (
18+
<article className="parallel-queries">
19+
<h2>Suspense: indipendent parallel queries</h2>
20+
<form
21+
className="select-pokemon-form"
22+
action="#"
23+
onSubmit={(evt) => {
24+
evt.preventDefault()
25+
26+
const formValues = new FormData(evt.currentTarget)
27+
28+
const next = Boolean(formValues.get('addBulbasaur'))
29+
? 'bulbasaur'
30+
: evolutionsKeys[
31+
Math.floor(Math.random() * evolutionsKeys.length)
32+
]
33+
34+
setEvolutions((curr) => curr.concat(next))
35+
}}
36+
>
37+
<label htmlFor="addBulbasaurandEvolution">
38+
addBulbasaur
39+
<input
40+
type="checkbox"
41+
name="addBulbasaur"
42+
id="addBulbasaurandEvolution"
43+
/>
44+
</label>
45+
<button type="submit">Add pokemon + evolution</button>
46+
</form>
47+
<div className="pokemon-list">
48+
{evolutions.map((name, idx) => (
49+
<ErrorBoundary
50+
key={idx}
51+
onError={console.error}
52+
fallbackRender={({ resetErrorBoundary, error }) => (
53+
<>
54+
<PokemonPlaceholder
55+
name={name}
56+
error={error}
57+
onRetry={() => {
58+
(error as any)?.retryQuery?.();
59+
resetErrorBoundary()
60+
}}
61+
/>
62+
<PokemonPlaceholder
63+
name={pokemonEvolutions[name]}
64+
error={error}
65+
onRetry={() => {
66+
(error as any)?.retryQuery?.()
67+
resetErrorBoundary()
68+
}}
69+
/>
70+
</>
71+
)}
72+
>
73+
<React.Suspense
74+
fallback={
75+
<>
76+
<PokemonPlaceholder name={name} />
77+
<PokemonPlaceholder name={pokemonEvolutions[name]} />
78+
</>
79+
}
80+
>
81+
<PokemonWithEvolution
82+
key={idx}
83+
base={name}
84+
evolution={pokemonEvolutions[name]}
85+
/>
86+
</React.Suspense>
87+
</ErrorBoundary>
88+
))}
89+
</div>
90+
</article>
91+
)
92+
}
93+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import * as React from 'react'
2+
import clsx from 'clsx'
3+
import { PokemonName } from './pokemon.data'
4+
5+
export interface PokemonPlaceholderProps
6+
extends React.HTMLAttributes<HTMLDivElement> {
7+
name: PokemonName
8+
error?: Error | undefined
9+
onRetry?(): void
10+
}
11+
12+
export function PokemonPlaceholder({
13+
name,
14+
children,
15+
className,
16+
error,
17+
onRetry,
18+
...otherProps
19+
}: PokemonPlaceholderProps) {
20+
const isError = !!error
21+
22+
let content: React.ReactNode = isError ? (
23+
<>
24+
<h3>An error has occurred while loading {name}</h3>
25+
<div>{error?.message}</div>
26+
{onRetry && (
27+
<button type="button" className="btn" onClick={onRetry}>
28+
retry
29+
</button>
30+
)}
31+
{children}
32+
</>
33+
) : (
34+
<>
35+
<h3>Loading pokemon {name}</h3>
36+
<br />
37+
(Suspense fallback)
38+
{children}
39+
</>
40+
)
41+
42+
return (
43+
<section
44+
className={clsx(
45+
'pokemon-card',
46+
'pokemon-cart--placeholder',
47+
{ 'alert--danger': isError, 'alert--info': !isError },
48+
className
49+
)}
50+
{...otherProps}
51+
>
52+
{content}
53+
</section>
54+
)
55+
}

0 commit comments

Comments
 (0)