Skip to content

Commit 75ad0bc

Browse files
timdorrhedgerh
authored andcommitted
Add an async example and documentation.
1 parent 987ad6f commit 75ad0bc

File tree

3 files changed

+91
-19
lines changed

3 files changed

+91
-19
lines changed

docs/recipes/ServerRendering.md

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,66 @@ function handleRender(req, res) {
190190
```
191191
The code reads from the Express `Request` object passed into our server middleware. The parameter is parsed into a number and then set in the initial state. If you visit [http://localhost:8080/?counter=100](http://localhost:8080/?counter=100) in your browser, you'll see the counter starts at 100. In the rendered HTML, you'll see the counter output as 100 and the `__INITIAL_STATE__` variable has the counter set in it.
192192
193-
### Async Data Fetching
193+
### Async State Fetching
194194
195-
Fetching data asynchronously during server side rendering is a common point of confusion. The first thing to understand is that you can fetch your data however you want, **as long as it is available _before_ we send our response to the client**.
195+
The most common issue with server side rendering is dealing with state that comes in asynchronously. Rendering on the server is synchronous by nature, so it's necessary to map any asynchronous fetches into a synchronous operation.
196196
197-
**examples**
197+
The easiest way to do this is to pass through some callback back to your synchronous code. In this case, that will be a function that will reference the response object and send back our rendered HTML to the client. Don't worry, it's not as hard as it may sound.
198+
199+
For our example, we'll imagine there is an external datastore that contains the counter's initial value (Counter As A Service, or CaaS). We'll make a mock call over to them and build our initial state from the result. We'll start by building out our API call:
200+
201+
**api/counter.js**
202+
203+
```js
204+
function getRandomInt(min, max) {
205+
return Math.floor(Math.random() * (max - min)) + min;
206+
}
207+
208+
export function fetchCounter(callback) {
209+
setTimeout(() => {
210+
callback(getRandomInt(1, 100));
211+
}, 500);
212+
}
213+
```
214+
215+
Again, this is just a mock API, so we use `setTimeout` to simulate a network request that takes 500 milliseconds to respond (this should be much faster with a real world API). We pass in a callback that returns a random number asynchronously. If you're using a Promise-based API client, then you would issue this callback in your `then` handler.
216+
217+
On the server side, we simply wrap our existing code in the `fetchCounter` and recieve the result in the callback:
218+
219+
**server.js**
220+
221+
```js
222+
// Add this to our imports
223+
import { fetchCounter } from './api/counter';
224+
225+
function handleRender(req, res) {
226+
227+
// Query our mock API asynchronously
228+
fetchCounter(apiResult => {
229+
230+
// Read the counter from the request, if provided
231+
const params = qs.parse(req.query);
232+
const counter = parseInt(params.counter) || apiResult || 0;
233+
234+
// Compile an initial state
235+
let initialState = { counter };
236+
237+
// Create a new Redux store instance
238+
const store = createStore(counterApp, initialState);
239+
240+
// Render the component to a string
241+
const html = React.renderToString(
242+
<Provider store={store}>
243+
{ () => <App/> }
244+
</Provider>);
245+
246+
// Grab the initial state from our Redux store
247+
const finalState = store.getState();
248+
249+
// Send the rendered page back to the client
250+
res.send(renderFullPage(html, finalState));
251+
});
252+
}
253+
```
254+
255+
Because we `res.send()` inside of the callback, the server will hold open the connection and won't send any data until that callback executes. You'll notice a 500ms delay is now added to each server request as a result of our new API call. A more advanced usage would handle errors in the API gracefully, such as a bad response or timeout.

examples/universal/api/counter.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
function getRandomInt(min, max) {
2+
return Math.floor(Math.random() * (max - min)) + min;
3+
}
4+
5+
export function fetchCounter(callback) {
6+
setTimeout(() => {
7+
callback(getRandomInt(1, 100));
8+
}, 500);
9+
}

examples/universal/server.js

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { createStore } from 'redux';
66
import { Provider } from 'react-redux';
77
import counterApp from './reducers';
88
import App from './containers/App';
9+
import { fetchCounter } from './api/counter';
910

1011
const app = Express();
1112
const port = 8080;
@@ -18,27 +19,31 @@ app.use(handleRender);
1819

1920
function handleRender(req, res) {
2021

21-
// Read the counter from the request, if provided
22-
const params = qs.parse(req.query);
23-
const counter = parseInt(params.counter) || 0;
22+
// Query our mock API asynchronously
23+
fetchCounter(apiResult => {
2424

25-
// Compile an initial state
26-
let initialState = { counter };
25+
// Read the counter from the request, if provided
26+
const params = qs.parse(req.query);
27+
const counter = parseInt(params.counter) || apiResult || 0;
2728

28-
// Create a new Redux store instance
29-
const store = createStore(counterApp, initialState);
29+
// Compile an initial state
30+
let initialState = { counter };
3031

31-
// Render the component to a string
32-
const html = React.renderToString(
33-
<Provider store={store}>
34-
{ () => <App/> }
35-
</Provider>);
32+
// Create a new Redux store instance
33+
const store = createStore(counterApp, initialState);
3634

37-
// Grab the initial state from our Redux store
38-
const finalState = store.getState();
35+
// Render the component to a string
36+
const html = React.renderToString(
37+
<Provider store={store}>
38+
{ () => <App/> }
39+
</Provider>);
3940

40-
// Send the rendered page back to the client
41-
res.send(renderFullPage(html, finalState));
41+
// Grab the initial state from our Redux store
42+
const finalState = store.getState();
43+
44+
// Send the rendered page back to the client
45+
res.send(renderFullPage(html, finalState));
46+
});
4247
}
4348

4449
function renderFullPage(html, initialState) {

0 commit comments

Comments
 (0)