Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 144 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ A big thanks to both [Draqula](https://github.com/vadimdemedes/draqula) for insp
- Playground (with devtools) - [CodeSandbox](https://codesandbox.io/s/github/tannerlinsley/react-query/tree/master/examples/playground) - [Source](./examples/playground)
- Star Wars (with devtools) - [CodeSandbox](https://codesandbox.io/s/github/tannerlinsley/react-query/tree/master/examples/star-wars) - [Source](./examples/star-wars)
- Rick And Morty (with devtools) - [CodeSandbox](https://codesandbox.io/s/github/tannerlinsley/react-query/tree/master/examples/rick-morty) - [Source](./examples/rick-morty)
- Prefetching - [Source](./examples/prefetching)
- Server Side Rendering - [Source](./examples/server-side-rendering)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would love to see codesandbox (using Next.js) for these

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been wanting to try out Codesandbox with a server for a while so that sounds good, I'll see what I can do.

I added the existing Prefetching example here just because I saw it was missing from the Readme. Adding Codesandbox for that here would be scope creep, want me to remove it again until someone can add a sandbox?

I wanted to include a "low level" SSR-example in this PR to demo the nitty gritty, I think a Next-example is the logical follow-up. I kind of think that example should also be included in this PR to demo that it's compatible, I'll go ahead and add that after the vacation!


## Sponsors

Expand Down Expand Up @@ -201,6 +203,7 @@ This library is being built and maintained by me, @tannerlinsley and I am always
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->


- [Installation](#installation)
- [Defaults to keep in mind](#defaults-to-keep-in-mind)
- [Queries](#queries)
Expand All @@ -220,7 +223,7 @@ This library is being built and maintained by me, @tannerlinsley and I am always
- [Initial Data](#initial-data)
- [Initial Data Function](#initial-data-function)
- [Initial Data from Cache](#initial-data-from-cache)
- [SSR & Initial Data](#ssr--initial-data)
- [Server Side Rendering](#server-side-rendering)
- [Suspense Mode](#suspense-mode)
- [Fetch-on-render vs Fetch-as-you-render](#fetch-on-render-vs-fetch-as-you-render)
- [Canceling Query Requests](#canceling-query-requests)
Expand Down Expand Up @@ -255,7 +258,9 @@ This library is being built and maintained by me, @tannerlinsley and I am always
- [`queryCache.getQueries`](#querycachegetqueries)
- [`queryCache.isFetching`](#querycacheisfetching)
- [`queryCache.subscribe`](#querycachesubscribe)
- [`queryCache.dehydrate`](#querycachedehydrate)
- [`queryCache.clear`](#querycacheclear)
- [`makeQueryCache` & `makeServerQueryCache`](#makequerycache--makeserverquerycache)
- [`useQueryCache`](#usequerycache)
- [`useIsFetching`](#useisfetching)
- [`ReactQueryConfigProvider`](#reactqueryconfigprovider)
Expand Down Expand Up @@ -912,12 +917,91 @@ function Todo({ todoId }) {
}
```

## SSR & Initial Data
## Server Side Rendering

With React Query, the default is to not cache queries on the server to avoid sensitive information leaking between requests. Also, because rendering on the server is currently a single pass synchronous process, the hooks `useQuery` etc does not trigger any data fetching.

To get around the above limitations, there is some extra setup involved if you want to get Server Side Rendering (SSR) working with React Query. See the example [Server Side Rendering](./examples/server-side-rendering) for a full working example with multiple pages using React Router.

### Server setup

When using SSR (server-side-rendering) with React Query there are a few things to note:
- For every request, create a `serverQueryCache` with `makeServerQueryCache()`
- Use `serverQueryCache.prefetchQuery` to prime the cache with the data you need to render
- `useQuery` and the other hooks will never fetch data on the server
- Any query you have not prefetched will instead be fetched on the client after first render
- Wrap your application in `<ReactQueryCacheProvider queryCache={serverQueryCache}>`
- Render your application
- Call `serverQueryCache.dehydrate()`, serialize the result and include that in the markup

- Query caches are not written to memory during SSR. This is outside of the scope of React Query and easily leads to out-of-sync data when used with frameworks like Next.js or other SSR strategies.
- Queries rendered on the server will by default use the `initialData` of an unfetched query. This means that by default, `data` will be set to `undefined`. To get around this in SSR, you can either pre-seed a query's cache data using the `config.initialData` option:
Minimal example, imagine `<App>` has a `useQuery('key', fetchAppData)`:

```js
import { renderToString } from 'react-dom/server'
import serialize from 'serialize-javascript'
import { makeServerQueryCache, ReactQueryCacheProvider } from 'react-query'
import App from './app'
import fetchAppData from './fetchAppData'

app.use('/*', async (req, res) => {
const serverQueryCache = makeServerQueryCache()

await serverQueryCache.prefetchQuery('key', fetchAppData)

const markup = renderToString(
<ReactQueryCacheProvider queryCache={serverQueryCache}>
<App />
</ReactQueryCacheProvider>
)

const serializedQueries = serialize(serverQueryCache.dehydrate())

// Note: Parts of the html-template omitted for brevity
res.send(`
<body>
<div id="root">${markup}</div>
<script>
window.__REACT_QUERY_DATA__ = ${serializedQueries}
</script>
</body>`)
})
```

### Client setup

- Parse the dehydrated queries from the markup
- Create a `queryCache` with `makeQueryCache({ initialQueries })`
- Wrap your application in `<ReactQueryCacheProvider queryCache={queryCache}>`
- Hydrate your application

```js
import { hydrate } from 'react-dom'
import { makeQueryCache } from 'react-query'
import App from './app'

const initialQueries = window.__REACT_QUERY_DATA__

const queryCache = makeQueryCache({ initialQueries })

hydrate(
<ReactQueryCacheProvider queryCache={queryCache}>
<App />
</ReactQueryCacheProvider>,
document.getElementById('root')
)
```

### Caveats (Advanced)

- Only successful queries are dehydrated
- If a query fails on the server, that will by default retry again on the client
- If you need to fail server rendering when a query fails, use the `throwOnError` option for that query
- Only the config for `staleTime` and `cacheTime` are dehydrated
- All other config is expected to be provided again on the client, for example directly when calling `useQuery()`
- `staleTime` and `cacheTime` are included to allow for prefetching queries on the server that do not immediately become stale on the client, for example when prefetching data not immediately used for rendering

### Alternative approach

If all you need is some pre-seeded data for your queries and letting them refetch on the client, you can avoid all the above setup and instead use one of the following approach to provide some `initialData` to a query:

```js
const { status, data, error } = useQuery('todos', fetchTodoList, {
Expand All @@ -942,6 +1026,8 @@ The query's state will still reflect that it is stale and has not been fetched y

React Query can also be used with React's new Suspense for Data Fetching API's. To enable this mode, you can set either the global or query level config's `suspense` option to `true`.

Suspense mode currently does not work combined with `makeServerQueryCache`.

Global configuration:

```js
Expand Down Expand Up @@ -1575,6 +1661,8 @@ function App() {

> An additional `stableStringify` utility is also exported to help with stringifying objects to have sorted keys.

> If you use `queryKeySerializerFn` together with `queryCache.dehydrate` you also need to pass a `queryKeyParserFn` to `makeQueryCache` like so: `makeQueryCache({ initialQueries, queryKeyParserFn })`

### URL Query Key Serializer Example

The example below shows how to build your own serializer for use with URLs and use it with React Query:
Expand Down Expand Up @@ -2248,6 +2336,8 @@ The `queryCache` instance is the backbone of React Query that manages all of the
- [`isFetching`](#querycacheisfetching)
- [`clear`](#querycacheclear)

The default use of `queryCache` is to import it directly from `react-query`. You can also create one with [`makeQueryCache`](#makequerycache--makeserverquerycache) or [`makeServerQueryCache`](#makequerycache--makeserverquerycache), pass the resulting cache to [`ReactQueryCacheProvider`](#reactquerycacheprovider) and access it via [`useQueryCache`](#usequerycache). This is mainly used for server side rendering or test isolation.

## `queryCache.prefetchQuery`

`prefetchQuery` is an asynchronous function that can be used to fetch and cache a query response before it is needed or fetched with `useQuery`.
Expand Down Expand Up @@ -2512,9 +2602,29 @@ const unsubscribe = queryCache.subscribe(callback)
- `unsubscribe: Function => void`
- This function will unsubscribe the callback from the query cache.

## `queryCache.dehydrate`

The `dehydrate` method can be used to get a serializeable representation of all _successful_ queries in the cache. This can be passed in as `initialQueries` to `make(Server)QueryCache` to create a warm cache where some queries are already available. This is mainly used to pass queries from the server to the client when doing server rendering.

Note that the result of this function is not in serialized form. You are responsible for any serialization, passing the data along and parsing.

> Warning: If you include serialized data in html-markup you can in some cases be vulnurable to XSS-injection, make sure you escape dangerous characters.

```js
import { queryCache } from 'react-query'

const dehydratedQueries = queryCache.dehydrate()
```

### Returns

- `queries: DehydratedQueries`
- Will be an object with the format `[queryHash]: DehydratedQuery`
- `DehydratedQuery` is a serializable representation of a query

## `queryCache.clear`

The `clear` method can be used to clear the queryCache entirely and start fresh.
The `clear` method can be used to clear the queryCache entirely and start fresh. This function also clears any active timeout and cancels all ongoing requests.

```js
import { queryCache } from 'react-query'
Expand All @@ -2527,6 +2637,34 @@ queryCache.clear()
- `queries: Array<Query>`
- This will be an array containing the queries that were found.

## `makeQueryCache` & `makeServerQueryCache`

`makeQueryCache` is a factory function that creates a `queryCache`. Just as with the global cache you can import directly, caches created by this function does not cache data in a server environment.

`makeServerQueryCache` is a factory function that creates a `queryCache` that caches data in a server environment. You should create a new cache per request to avoid leaking sensitive information between requests.

```js
import { makeQueryCache } from 'react-query'

const queryCache = makeQueryCache({
initialQueries,
queryKeyParserFn,
})
```

### Options

- `initialQueries: DehydratedQueries`
- If you provide the optional `initialQueries`, these queries will be warm in the cache from the start
- These are obtained by calling `queryCache.dehydrate` on a cache
- `queryKeyParserFn: Function(queryHash: string) => QueryKey`
- This is an optional advanced option that you only need to provide if you use the experimental `queryKeySerializerFn` config-option
- This function should parse a `QueryHash` back into a `QueryKey`

### Returns

- `queryCache: QueryCache`

## `useQueryCache`

The `useQueryCache` hook returns the current queryCache instance.
Expand Down
12 changes: 12 additions & 0 deletions examples/server-side-rendering/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
logs
*.log
npm-debug.log*
.DS_Store

coverage
node_modules
build
.env.local
.env.development.local
.env.test.local
.env.production.local
6 changes: 6 additions & 0 deletions examples/server-side-rendering/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Example

To run this example:

- `npm install` or `yarn`
- `npm run start` or `yarn start`
22 changes: 22 additions & 0 deletions examples/server-side-rendering/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "react-query-server-side-rendering-example",
"version": "0.1.0",
"license": "MIT",
"scripts": {
"start": "razzle start",
"build": "razzle build",
"test": "razzle test --env=jsdom",
"start:prod": "NODE_ENV=production node build/server.js"
},
"dependencies": {
"express": "^4.17.1",
"isomorphic-fetch": "^2.2.1",
"razzle": "^3.1.3",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-query": "^1.5.5",
"react-router-config": "^5.1.1",
"react-router-dom": "^5.2.0",
"serialize-javascript": "^4.0.0"
}
}
Binary file added examples/server-side-rendering/public/favicon.ico
Binary file not shown.
2 changes: 2 additions & 0 deletions examples/server-side-rendering/public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
User-agent: *

26 changes: 26 additions & 0 deletions examples/server-side-rendering/src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
body {
margin: 0;
padding: 0;
font-family: sans-serif;
background-color: #f0f4f8;
}

.App-container {
max-width: 500px;
margin: 0 auto;
min-height: 100vh;
background-color: #ffffff;
box-shadow: 0px 0px 2px #102a43;
}

.App-header {
text-align: center;
background-color: #222;
min-height: 70px;
padding: 20px;
color: white;
}

.App-content {
padding: 12px 24px;
}
24 changes: 24 additions & 0 deletions examples/server-side-rendering/src/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react'
import { Route, Switch } from 'react-router-dom'
import routes from './routes'

import './App.css'

const App = () => (
<div className="App-container">
<div>
<div className="App-header">
<h2>React Query Server Side Rendering Example</h2>
</div>
<div className="App-content">
<Switch>
{routes.map((routeConfig, index) => (
<Route key={index} {...routeConfig} />
))}
</Switch>
</div>
</div>
</div>
)

export default App
40 changes: 40 additions & 0 deletions examples/server-side-rendering/src/Character.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react'
import { Link, useParams } from 'react-router-dom'
import { useQuery } from 'react-query'
import fetch from './fetch'

const fetchCharacter = (key, characterId) =>
fetch(`https://rickandmortyapi.com/api/character/${characterId}`)

function Character() {
const { characterId } = useParams()
const { status, data } = useQuery(['character', characterId], fetchCharacter)

let character
if (status === 'loading') {
character = <p>Loading ...</p>
} else if (status === 'error') {
character = <p>Error :(</p>
} else if (status === 'success' && data) {
character = (
<>
<h1>{data.name}</h1>
<p>Gender: {data.gender}</p>
<p>Status: {data.status}</p>
<p>Species: {data.species}</p>
</>
)
}

return (
<>
<Link to="/">Start</Link>
{character}
</>
)
}

Character.prefetch = (queryCache, params) =>
queryCache.prefetchQuery(['character', params.characterId], fetchCharacter)

export default Character
Loading