Skip to content

Commit 31533a7

Browse files
committed
feat(rtk-query/react): add useUnstable_SuspenseQuery & suspense example
-- Description Adds useUnstable_SuspenseQuery, an alternative to `useQuery` that can be used to render-as-you-fetch. -- References - https://reactjs.org/docs/concurrent-mode-suspense.html - https://github.com/facebook/react/blob/12adaffef7105e2714f82651ea51936c563fe15c/packages/react/src/ReactLazy.js -- Alternatives - swr: https://swr.vercel.app/docs/suspense - react-query: https://react-query.tanstack.com/guides/suspense
1 parent 1751eb9 commit 31533a7

File tree

18 files changed

+728
-0
lines changed

18 files changed

+728
-0
lines changed

examples/query/react/suspense/.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SKIP_PREFLIGHT_CHECK=true
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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.6.0-rc.1",
10+
"react": "17.0.0",
11+
"react-dom": "17.0.0",
12+
"react-error-boundary": "3.1.4",
13+
"react-redux": "7.2.2",
14+
"react-scripts": "4.0.2"
15+
},
16+
"devDependencies": {
17+
"@types/react": "17.0.0",
18+
"@types/react-dom": "17.0.0",
19+
"@types/react-redux": "7.1.9",
20+
"typescript": "~4.2.4"
21+
},
22+
"eslintConfig": {
23+
"extends": [
24+
"react-app"
25+
],
26+
"rules": {
27+
"react/react-in-jsx-scope": "off"
28+
}
29+
},
30+
"scripts": {
31+
"start": "react-scripts start",
32+
"build": "react-scripts build"
33+
},
34+
"browserslist": [
35+
">0.2%",
36+
"not dead",
37+
"not ie <= 11",
38+
"not op_mini all"
39+
]
40+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="utf-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
7+
<meta name="theme-color" content="#000000">
8+
<!--
9+
manifest.json provides metadata used when your web app is added to the
10+
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
11+
-->
12+
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
13+
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
14+
<!--
15+
Notice the use of %PUBLIC_URL% in the tags above.
16+
It will be replaced with the URL of the `public` folder during the build.
17+
Only files inside the `public` folder can be referenced from the HTML.
18+
19+
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
20+
work correctly both with client-side routing and a non-root public URL.
21+
Learn how to configure a non-root public URL by running `npm run build`.
22+
-->
23+
<title>React App</title>
24+
</head>
25+
26+
<body>
27+
<noscript>
28+
You need to enable JavaScript to run this app.
29+
</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+
43+
</html>
Lines changed: 8 additions & 0 deletions
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+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import * as React from 'react'
2+
import { POKEMON_NAMES } from './pokemon.data'
3+
import './styles.css'
4+
import { SuspendedPokemon, SuspendedPokemonProps } from './SuspendedPokemon'
5+
6+
const getRandomPokemonName = () =>
7+
POKEMON_NAMES[Math.floor(Math.random() * POKEMON_NAMES.length)]
8+
9+
export default function App() {
10+
const [pokemonConf, setPokemonConf] = React.useState<SuspendedPokemonProps[]>(
11+
[{ name: 'bulbasaur', suspendOnRefetch: false, throwOnIntialRender: false }]
12+
)
13+
14+
return (
15+
<div className="App">
16+
<div>
17+
<form
18+
action="#"
19+
onSubmit={(evt) => {
20+
evt.preventDefault()
21+
22+
const formValues = new FormData(evt.currentTarget)
23+
24+
setPokemonConf((prev) => [
25+
...prev,
26+
{
27+
name: Boolean(formValues.get('addBulbasaur'))
28+
? 'bulbasaur'
29+
: getRandomPokemonName(),
30+
suspendOnRefetch: Boolean(formValues.get('suspendOnRefetch')),
31+
throwOnIntialRender: Boolean(
32+
formValues.get('throwOnIntialRender')
33+
),
34+
},
35+
])
36+
}}
37+
>
38+
<label htmlFor="suspendOnRefetch">
39+
suspendOnRefetch
40+
<input
41+
type="checkbox"
42+
name="suspendOnRefetch"
43+
id="suspendOnRefetch"
44+
/>
45+
</label>
46+
<label htmlFor="addBulbasaur">
47+
addBulbasaur
48+
<input type="checkbox" name="addBulbasaur" id="addBulbasaur" />
49+
</label>
50+
<label htmlFor="throwOnIntialRender">
51+
throwOnIntialRender
52+
<input
53+
type="checkbox"
54+
name="throwOnIntialRender"
55+
id="throwOnIntialRender"
56+
/>
57+
</label>
58+
<button>Add pokemon</button>
59+
</form>
60+
</div>
61+
<div className="pokemon-list">
62+
{pokemonConf.map((suspendedPokemonProps, index) => (
63+
<SuspendedPokemon key={index} {...suspendedPokemonProps} />
64+
))}
65+
</div>
66+
</div>
67+
)
68+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import * as React from 'react'
2+
import { pokemonApi } from './services/pokemon'
3+
import type { PokemonName } from './pokemon.data'
4+
5+
const intervalOptions = [
6+
{ label: 'Off', value: 0 },
7+
{ label: '20s', value: 10000 },
8+
{ label: '1m', value: 60000 },
9+
]
10+
11+
const getRandomIntervalValue = () =>
12+
intervalOptions[Math.floor(Math.random() * intervalOptions.length)].value
13+
14+
export const Pokemon = ({
15+
name,
16+
suspendOnRefetch = false,
17+
}: {
18+
name: PokemonName
19+
suspendOnRefetch?: boolean
20+
}) => {
21+
const [pollingInterval, setPollingInterval] = React.useState(
22+
getRandomIntervalValue()
23+
)
24+
25+
const { data, isFetching, refetch } =
26+
pokemonApi.endpoints.getPokemonByName.useUnstable_SuspenseQuery(name, {
27+
pollingInterval,
28+
suspendOnRefetch,
29+
})
30+
31+
if (!data) {
32+
return (
33+
<section>
34+
<h3>{name}</h3>
35+
<p>No data!</p>
36+
</section>
37+
)
38+
}
39+
40+
return (
41+
<section>
42+
<h3>{data.species.name}</h3>
43+
<div style={{ minWidth: 96, minHeight: 96 }}>
44+
<img
45+
src={data.sprites.front_shiny}
46+
alt={data.species.name}
47+
style={{ ...(isFetching ? { opacity: 0.3 } : {}) }}
48+
/>
49+
</div>
50+
<div>
51+
<label style={{ display: 'block' }}>Polling interval</label>
52+
<select
53+
value={pollingInterval}
54+
onChange={({ target: { value } }) =>
55+
setPollingInterval(Number(value))
56+
}
57+
>
58+
{intervalOptions.map(({ label, value }) => (
59+
<option key={value} value={value}>
60+
{label}
61+
</option>
62+
))}
63+
</select>
64+
</div>
65+
<div>
66+
<p>suspendOnRefetch: {String(suspendOnRefetch)}</p>
67+
<button onClick={refetch} disabled={isFetching}>
68+
{isFetching ? 'Loading' : 'Manually refetch'}
69+
</button>
70+
</div>
71+
</section>
72+
)
73+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Suspense, useState } from 'react'
2+
import { Pokemon } from './Pokemon'
3+
import { PokemonName } from './pokemon.data'
4+
import { ErrorBoundary } from 'react-error-boundary'
5+
6+
export interface SuspendedPokemonProps {
7+
name: PokemonName
8+
suspendOnRefetch: boolean
9+
throwOnIntialRender: boolean
10+
}
11+
12+
function BuggyComponent({ errorCount, name }:Pick<SuspendedPokemonProps, 'name'> & { errorCount: number }) {
13+
if(!errorCount) {
14+
throw new Error('error while rendering:' + name)
15+
}
16+
17+
return <></>
18+
}
19+
20+
export function SuspendedPokemon({
21+
name,
22+
suspendOnRefetch,
23+
throwOnIntialRender,
24+
}: SuspendedPokemonProps) {
25+
const [errorCount, setErrorCount] = useState(0)
26+
27+
return (
28+
<div>
29+
<ErrorBoundary
30+
onReset={() => setErrorCount((n) => n + 1)}
31+
fallbackRender={({ resetErrorBoundary, error }) => {
32+
return (
33+
<section>
34+
<h3>render {name} error</h3>
35+
<p>{String(error)}</p>
36+
<div>
37+
<button type="button" onClick={resetErrorBoundary}>
38+
reset error boundary
39+
</button>
40+
</div>
41+
</section>
42+
)
43+
}}
44+
>
45+
{throwOnIntialRender && <BuggyComponent name={name} errorCount={errorCount} />}
46+
<Suspense
47+
fallback={
48+
<div>
49+
suspense fallback <br />
50+
loading pokemon {name}
51+
</div>
52+
}
53+
>
54+
<Pokemon name={name} suspendOnRefetch={suspendOnRefetch} />
55+
</Suspense>
56+
</ErrorBoundary>
57+
</div>
58+
)
59+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { render } from 'react-dom'
2+
import { Provider } from 'react-redux'
3+
4+
import App from './App'
5+
import { store } from './store'
6+
7+
const rootElement = document.getElementById('root')
8+
render(
9+
<Provider store={store}>
10+
<App />
11+
</Provider>,
12+
rootElement
13+
)

0 commit comments

Comments
 (0)