So far, we have built small applications with React that run entirely in the browser. They have collected data in the browser and saved the data using browser storage. This makes sense because React is a view layer. It is intended to render UI. However, most applications require at least the existence of some sort of a backend, and we will need to understand how to structure applications with a server in mind.
Even if you have a client application that is relying entirely on cloud services for the backend, you still need to get and send data to these services. Within the scope of Flux, there are specific places where these transactions should be made, and libraries that can help you deal with the latency associated with HTTP requests.
Additionally React can be rendered isomorphically, which means that it can be in platforms other than the browser. This means we can render our UI on the server before it ever gets to the browser. Taking advantage of server rendering, we can improve the performance, portability, and security of our applications.
We start this chapter with a look at the differences between isomorphism and universalism and how both concepts relate to React. Next, we will look at how to make an isomorphic application using universal JavaScript. Finally, we will improve the color organizer by adding a server and rendering the UI on the server first.
The terms isomorphic and universal are often used to describe applications that work on both the client and the server. Although these terms are used interchangeably to describe the same application, there is a subtle difference between them that is worth investigating. Isomorphic applications are applications that can be rendered on multiple platforms. Universal code means that the exact same code can run in multiple environments.1
Node.js will allow us to reuse the same code that we’ve written in the browser in other applications such as servers, CLIs, and even native applications. Let’s take a look at some universal JavaScript:
var
printNames
=
response
=>
{
var
people
=
JSON
.
parse
(
response
).
results
,
names
=
people
.
map
(({
name
})
=>
`
${
name
.
last
}
,
${
name
.
first
}
`
)
console
.
log
(
names
.
join
(
' '
))
}
The printNames
function is universal. The exact same code can be invoked in the browser or on a server. This means that if we constructed a server with Node.js, we could potentially reuse a lot of code between the two environments. Universal JavaScript is JavaScript that can run on the server or in the browser without error (Figure 12-1).
The server and the client are completely different domains, so all of your JavaScript code will not automatically work between them. Let’s take a look at creating an AJAX request with the browser:
const
request
=
new
XMLHttpRequest
()
request
.
open
(
'GET'
,
'https://api.randomuser.me/?nat=US&results=10'
)
request
.
onload
=
()
=>
printNames
(
request
.
response
)
request
.
send
()
Here we are requesting 10 user records at random from the randomuser.me API. If we run this code in the browser and look at the console, we will see 10 random names:
ford
,
brianna
henderson
,
nellie
lynch
,
lily
gordon
,
todd
collins
,
genesis
roberts
,
suzanne
dixon
,
rene
ray
,
rafael
adams
,
jamie
bowman
,
mia
However, if we try to run the exact same code with Node.js, we get an error:
ReferenceError: XMLHttpRequest is not defined at Object.<anonymous> (/Users/...) at Module._compile (module.js:541:32) at Object.Module._extensions..js (module.js:550:10) at Module.load (module.js:458:32) at tryModuleLoad (module.js:417:12) at Function.Module._load (module.js:409:3) at Function.Module.runMain (module.js:575:10) at startup (node.js:160:18) at node.js:449:3
This error occurs because Node.js does not have an XMLHttpRequest
object like the browser does. With Node.js, we can use the http
module to make a request:
const
https
=
require
(
'https'
)
https
.
get
(
'https://api.randomuser.me/?nat=US&results=10'
,
res
=>
{
let
results
=
''
res
.
setEncoding
(
'utf8'
)
res
.
on
(
'data'
,
chunk
=>
results
+=
chunk
)
res
.
on
(
'end'
,
()
=>
printNames
(
results
))
}
)
Loading data from an API with Node.js requires the use of core modules. It requires different code. In these samples, the printNames
function is universal, so the same function works in both environments.
We could build a module that would print the names to the console in either a browser or a Node.js application:
var
printNames
=
response
=>
{
var
people
=
JSON
.
parse
(
response
).
results
,
names
=
people
.
map
(({
name
})
=>
`
${
name
.
last
}
,
${
name
.
first
}
`
)
console
.
log
(
names
.
join
(
' '
))
}
if
(
typeof
window
!==
'undefined'
)
{
const
request
=
new
XMLHttpRequest
()
request
.
open
(
'GET'
,
'http://api.randomuser.me/?nat=US&results=10'
)
request
.
onload
=
()
=>
printNames
(
request
.
response
)
request
.
send
()
}
else
{
const
https
=
require
(
'https'
)
https
.
get
(
'http://api.randomuser.me/?nat=US&results=10'
,
res
=>
{
let
results
=
''
res
.
setEncoding
(
'utf8'
)
res
.
on
(
'data'
,
chunk
=>
results
+=
chunk
)
res
.
on
(
'end'
,
()
=>
printNames
(
results
))
}
)
}
This JavaScript file is now isomorphic; it contains universal JavaScript. All of the code is not universal, but the file itself will work in both environments. It can run it with Node.js or include it in a <script>
tag in the browser.
We have been using isomorphic-fetch
over other implementations of the WHATWG fetch function because isomorphic-fetch
works in multiple environments.
Let’s take a look at the Star
component. Is this component universal?
const
Star
=
({
selected
=
false
,
onClick
=
f
=>
f
})
=>
<
div
className
=
{(
selected
)
?
"star selected"
:
"star"
}
onClick
=
{
onClick
}
>
<
/div>
Sure it is: remember, the JSX compiles to JavaScript. The Star
component is simply a function:
const
Star
=
({
selected
=
false
,
onClick
=
f
=>
f
})
=>
React
.
createElement
(
"div"
,
{
className
:
selected
?
"star selected"
:
"star"
,
onClick
:
onClick
}
)
We can render this component directly in the browser, or render it in a different environment and capture the HTML output as a string. ReactDOM
has a renderToString
method that we can use to render UI to a HTML string:
// Renders html directly in the browser ReactDOM.render(<Star />) // Renders html as a string var html = ReactDOM.renderToString(<Star />)
We can build isomorphic applications that render components on different platforms, and we can architect these applications in a way that reuses JavaScript code universally across multiple environments. Additionally, we can build isomorphic applications using other languages such as Go or Python. We are not restricted to Node.js.
Using the ReactDOM.renderToString
method allows us to render UI on the server. Servers are powerful; they have access to all kinds of resources that browsers do not. Servers can be secure, and access secure data. You can use all of these added benefits to your advantage by rendering initial content on the server.
Let’s build a basic web server using Node.js and Express. Express is a library that we can use to rapidly develop web servers:
npm install express --save
Let’s take a look at a simple Express app. This code creates a web server that always serves the message “Hello World”. First, information about each request is logged to the console. Then the server responds with some HTML. Both of these steps are contained in their own function and chained together with the .use()
method. Express automatically injects request and response arguments into each of these functions as arguments.
import
express
from
'express'
const
logger
=
(
req
,
res
,
next
)
=>
{
console
.
log
(
`
${
req
.
method
}
request for '
${
req
.
url
}
'`
)
next
()
}
const
sayHello
=
(
req
,
res
)
=>
res
.
status
(
200
).
send
(
"<h1>Hello World</h1>"
)
const
app
=
express
()
.
use
(
logger
)
.
use
(
sayHello
)
app
.
listen
(
3000
,
()
=>
console
.
log
(
`Recipe app running at 'http://localhost:3000'`
)
)
The logger
and sayHello
functions are middleware. In Express, middleware functions are chained together into a pipeline with the .use()
method.2 When a request occurs, each middleware function is invoked in order until a response is sent. This Express app logs details about each request to the console and then sends an HTML response: <h1>Hello World</h1>
. Finally, we start the Express app by telling it to listen for incoming requests locally on port 3000.
In Chapter 10 we used the babel-cli
to run our tests. Here we will use the babel-cli
to run this Express app because it contains ES6 import statements that are not supported by the current version of Node.js.
babel-cli
is not a great solution for running apps in production, and we don’t have to use to babel-cli
to run every Node.js app that uses ES6. As of this writing, the current version of Node.js supports a lot of ES6 syntax. You could simply choose not to use import statements. Future versions of Node.js will support import statements.
Another option is to create a webpack build for your backend code. webpack can export a JavaScript bundle that can be ran with older versions of Node.js.
In order to run babel-node
, there is a little bit of setup involved. First, we need to install the babel-cli
, babel-loader
, babel-preset-es2015
, babel-preset-react
, and babel-preset-stage-0
:
npm install babel-cli babel-loader babel-preset-env babel-preset-react babel-preset-stage-0 --save
Next, we need to make sure we add a .babelrc file to the root of our project. When we run babel-node index-server.js
, Babel will look for this file and apply the presets that we have installed:
{
"presets"
:
[
"env"
,
"stage-0"
,
"react"
]
}
Finally, let’s add a start
script to our package.json file. If you do not already have a package.json file, create one by running npm init
:
"scripts"
:
{
"start"
:
"./node_modules/.bin/babel-node index-server.js"
}
Now we can run our Express server with the command npm start
:
npm
start
Recipe
app
running
at
'http://localhost:3000'
Once the server is running, you can open a web browser and navigate to http://localhost:3000. You will see the message “Hello World” in the page.
ctrl^c
will stop this Express server from running.
So far, our Express app responds to all requests with the same string: "<h1>Hello World</h1>"
. Instead of rendering this message, let’s render the Recipe app that we worked with back in Chapters 4 and 5. We can make this modification by rendering the Menu
component with some recipe data using renderToString
from ReactDOM:
import
React
from
'react'
import
express
from
'express'
import
{
renderToString
}
from
'react-dom/server'
import
Menu
from
'./components/Menu'
import
data
from
'./assets/recipes.json'
global
.
React
=
React
const
html
=
renderToString
(
<
Menu
recipes
=
{
data
}
/
>
)
const
logger
=
(
req
,
res
,
next
)
=>
{
console
.
log
(
`
${
req
.
method
}
request for '
${
req
.
url
}
'
`
)
next
(
)
}
const
sendHTMLPage
=
(
req
,
res
)
=>
res
.
status
(
200
)
.
send
(
`
<!DOCTYPE html> <html> <head> <title>React Recipes App</title> </head> <body> <div id="react-container">
${
html
}
</div> </body> </html>
`
)
const
app
=
express
(
)
.
use
(
logger
)
.
use
(
sendHTMLPage
)
app
.
listen
(
3000
,
(
)
=>
console
.
log
(
`
Recipe app running at 'http://localhost:3000'
`
)
)
First we import react
, the renderToString
method, the Menu
component, and some recipes for our initial data. React is exposed globally, so the renderToString
method can work properly.
Next, the HTML is obtained by invoking the renderToString
function and sending it the Menu
component.
Finally, we can create a new middleware function, sendHTMLPage
, that responds to all requests with an HTML string. This string wraps the server-rendered HTML in boilerplate that is necessary for creating a page.
Now when you start this application and navigate to http://localhost:3000 in a browser, you will see that the recipes have been rendered. We have not included any JavaScript in this response. The recipes are already on the page as HTML.
So far we have server-rendered the Menu
component. Our application is not yet isomorphic, as the components are only rendered on the server. To make it isomorphic we will add some JavaScript to the response so that the same components can be rendered in the browser.
Let’s create an index-client.js file that will run in the browser:
import
React
from
'react'
import
{
render
}
from
'react-dom'
import
Menu
from
'./components/Menu'
window
.
React
=
React
alert
(
'bundle loaded, Rendering in browser'
)
render
(
<
Menu
recipes
=
{
__DATA__
}
/
>
,
document
.
getElementById
(
"react-container"
)
)
alert
(
'render complete'
)
This file will render the same Menu
component, with the same recipe data. We know that the data is the same because it will already be included in our response as a string. When the browser loads this script, the __DATA__
will already exist in the global scope. The alert
methods are used to see when the browser renders the UI.
We’ll need to build this client.js file into a bundle that can be used by the browser. Here, basic webpack configuration will handle the build.
Don’t forget to install webpack
; we’ve already installed babel
and the necessary presets:
npm install webpack --save-dev
Here, basic webpack configuration will handle the build:
var
webpack
=
require
(
"webpack"
)
module
.
exports
=
{
entry
:
"./index-client.js"
,
output
:
{
path
:
"assets"
,
filename
:
"bundle.js"
},
module
:
{
rules
:
[
{
test
:
/.js$/
,
exclude
:
/(node_modules)/
,
loader
:
'babel-loader'
,
query
:
{
presets
:
[
'env'
,
'stage-0'
,
'react'
]
}
}
]
}
}
We want to build the client bundle every time we start our app, so we’ll need to add a prestart script to the package.json file:
"scripts"
:
{
"prestart"
:
"./node_modules/.bin/webpack --progress"
,
"start"
:
"./node_modules/.bin/babel-node index-server.js"
},
The last step is to modify the server. We need to write the initial __DATA__
to the response as a string. We also need to include a script
tag with a reference to our client bundle. Lastly, we need to make sure our server sends static files from the ./assets/ directory:
const
sendHTMLPage
=
(
req
,
res
)
=>
res
.
status
(
200
)
.
send
(
`
<!DOCTYPE html> <html> <head> <title>React Recipes App</title> </head> <body> <div id="react-container">
${
html
}
</div>
<script> window.__DATA__ =
${
JSON
.
stringify
(
data
)
}
</script> <script src="bundle.js"></script>
</body> </html>
`
)
const
app
=
express
(
)
.
use
(
logger
)
.
use
(
express
.
static
(
'./assets'
)
)
.
use
(
sendHTMLPage
)
script
tags have been added directly to the response. The data is written to the first script
tag and the bundle is loaded in the second one. Additionally, middleware has been added to our request pipeline. When the /bundle.js file is requested, the express.static
middleware will respond with that file instead of the server-rendered HTML because it is in the ./assets folder.
Now we are isomorphically rendering the React components, first on the server and then in the browser. When you run this app, you will see alert pop ups before and after the components are rendered in the browser. You may notice that before you clear the first alert, the content is already there. This is because it is initially rendered on the server.
It may seem silly to render the same content twice, but there are advantages. This application renders the same content in all browsers, even if JavaScript is turned off. Because the content is loaded with the initial request, your website will run faster and deliver necessary content to your mobile users more quickly.3 It will not have to wait for a mobile processor to render the UI—the UI is already in place. Additionally, this app gains all of the advantages of an SPA. Isomorphic React applications give you the best of both worlds.
In the last five chapters, we have been working on a color organization application. Thus far, we’ve generated a lot of code base for this application, and it all runs in the browser. We’ve coded React components, a Redux store, and tons of action creators and helper functions. We’ve even incorporated the React Router. We already have a lot of code that can be reused if we were to create a web server.
Let’s create an Express server for this application and try to reuse as much code as possible. First, we’ll need a module that configures an Express application instance, so let’s create ./server/app.js:
import
express
from
'express'
import
path
from
'path'
import
fs
from
'fs'
const
fileAssets
=
express
.
static
(
path
.
join
(
__dirname
,
'../../dist/assets'
)
)
const
logger
=
(
req
,
res
,
next
)
=>
{
console
.
log
(
`
${
req
.
method
}
request for '
${
req
.
url
}
'`
)
next
()
}
const
respond
=
(
req
,
res
)
=>
res
.
status
(
200
).
send
(
`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Universal Color Organizer</title>
</head>
<body>
<div id="react-container">ready...</div>
</body>
</html>
`
)
export
default
express
()
.
use
(
logger
)
.
use
(
fileAssets
)
.
use
(
respond
)
This module is the starting point for our universal application. The Express configuration uses middleware for logging and file assets, and eventually it responds to every request with an HTML page.
Since we are serving HTML directly from this file, you’ll need to remove the ./dist/index.html file. If this file remains in place, it will be served before the response
is reached.
Webpack allows us to import assets like CSS or image files, but Node.js will not know how to handle those imports. We’ll need to use the ignore-styles
library to make sure that we are ignoring any SCSS import statements. Let’s install ignore-styles
:
npm install ignore-styles --save
In the ./src/server/index.js file, we will consume the Express app instance and start the server. This file represents the entry point for our Node.js server:
import
React
from
'react'
import
ignoreStyles
from
'ignore-styles'
import
app
from
'./app'
global
.
React
=
React
app
.
set
(
'port'
,
process
.
env
.
PORT
||
3000
)
.
listen
(
app
.
get
(
'port'
),
()
=>
console
.
log
(
'Color Organizer running'
)
)
This file adds React to the global instance and starts the server. Additionally, we’ve included the ignore-styles
module, which ignores those imports so we can render components in Node.js without causing errors.
We now have a starting point: a basic Express app configuration. Any time we need to include new features on the server, they will need to make their way into this app configuration module.
For the remainder of this chapter, we will iterate on this Express application. We will use code universally to create an isomorphic/universal version of the color organizer.
All of the JavaScript in the Redux library is universal. Your reducers are written in JavaScript and should not contain code that depends upon any environment. Redux was designed to be used as a state container for browser applications, but it can be used in all types of Node.js applications, including CLIs, servers, and native applications, too.
We already have the code in place for the Redux store. We’ll use this store to save state changes to a JSON file on the server.
First, we need to modify the storeFactory
so that it can work isomorphically. At present, the storeFactory
includes logging middleware that will cause errors in Node.js because it utilizes the console.groupCollapsed
and console.groupEnd
methods. Neither of these methods are available in Node.js. If we create stores on the server, we’ll need to use a different logger:
import
{
colors
}
from
'./reducers'
import
{
createStore
,
combineReducers
,
applyMiddleware
}
from
'redux'
const
clientLogger
=
store
=>
next
=>
action
=>
{
let
result
console
.
groupCollapsed
(
"dispatching"
,
action
.
type
)
console
.
log
(
'prev state'
,
store
.
getState
(
)
)
console
.
log
(
'action'
,
action
)
result
=
next
(
action
)
console
.
log
(
'next state'
,
store
.
getState
(
)
)
console
.
groupEnd
(
)
return
result
}
const
serverLogger
=
store
=>
next
=>
action
=>
{
console
.
log
(
' dispatching server action '
)
console
.
log
(
action
)
console
.
log
(
' '
)
return
next
(
action
)
}
const
middleware
=
server
=>
(
server
)
?
serverLogger
:
clientLogger
const
storeFactory
=
(
server
=
false
,
initialState
=
{
}
)
=>
applyMiddleware
(
middleware
)
(
createStore
)
(
combineReducers
(
{
colors
}
)
,
initialState
)
export
default
storeFactory
Now the storeFactory
is isomorphic. We created Redux middleware for logging actions on the server. When the storeFactory
is invoked, we’ll tell it which type of store we want and the appropriate logger will be added to the new store instance.
Let’s now use this isomorphic storeFactory
to create a serverStore
instance. At the top of the Express configuration, we’ll need to import the storeFactory
and the initial state data. We can use the storeFactory
to create a store with initial state from a JSON file:
import
storeFactory
from
'../store'
import
initialState
from
'../../data/initialState.json'
const
serverStore
=
storeFactory
(
true
,
initialState
)
Now we have an instance of the store that will run on the server.
Every time an action is dispatched to this instance, we want to make sure the initialState.json file is updated. Using the subscribe
method, we can listen for state changes and save a new JSON file every time the state changes:
serverStore
.
subscribe
(()
=>
fs
.
writeFile
(
path
.
join
(
__dirname
,
'../../data/initialState.json'
),
JSON
.
stringify
(
serverStore
.
getState
()),
error
=>
(
error
)
?
console
.
log
(
"Error saving state!"
,
error
)
:
null
)
)
As actions are dispatched, the new state is saved to the initialState.json file using the fs
module.
The serverStore
is now the main source of truth. Any requests will need to communicate with it in order to get the current and most up-to-date list of colors. We’ll add some middleware that will add the server store to the request pipeline so that it can be used by other middleware during a request:
const
addStoreToRequestPipeline
=
(
req
,
res
,
next
)
=>
{
req
.
store
=
serverStore
next
(
)
}
export
default
express
(
)
.
use
(
logger
)
.
use
(
fileAssets
)
.
use
(
addStoreToRequestPipeline
)
.
use
(
htmlResponse
)
Now any middleware method that comes after addStoreToRequestPipeline
will have access to the store on the request
object. We have used Redux universally. The exact same code for the store, including our reducers, will run in multiple environments.
There are complications associated with building web servers for large applications that are not addressed by this example. Saving data to a JSON file is a quick solution for data persistence, but production applications use actual databases. Using Redux is a possible solution that may meet requirements for some applications. However, there are complications associated with forking node processes that need to be addressed in larger applications. You can investigate solutions like Firebase and other cloud providers for assistance in working with databases that can scale smoothly.
In the last chapter, we added the react-router-dom
to the color organizer. The router decides which component to render based on the browser’s current location. The router can be rendered on the server as well—we just need to provide the location or route.
So far, we’ve used the HashRouter
. The router automatically adds a #
before each route. To use the router isomorphically, we need to replace the HashRouter
with the BrowserRouter
, which removes the preceding #
from our routes.
When we render our application, we need to replace the HashRouter
with the BrowserRouter
:
import
{
BrowserRouter
}
from
'react-router-dom'
...
render
(
<
Provider
store
=
{
store
}
>
<
BrowserRouter
>
<
App
/
>
<
/
B
r
o
w
s
e
r
R
o
u
t
e
r
>
<
/
P
r
o
v
i
d
e
r
>
,
document
.
getElementById
(
'react-container'
)
)
Now the color organizer is no longer prefacing each route with a hash. At this point, the organizer still works. Start it up and select one color. The Color
container is rendered, and it changes the background of the entire screen using the ColorDetails
component.
The location bar should now look something like:
http://localhost:3000/8658c1d0-9eda-4a90-95e1-8001e8eb6036
There is no longer a # in front of the route. Now let’s refresh the page in the browser:
Cannot GET /8658c1d0-9eda-4a90-95e1-8001e8eb6036
Refreshing the page causes the browser to send a GET request to the server using the current route. The #
was used to prevent us from sending that GET request. We use the BrowserRouter
because we want the GET request to be sent to the server. In order to render the router on the server, we need a location—we need the route. This route will be used on the server to tell the router to render the Color
container. The BrowserRouter
is used when you want to render routes isomorphically.
Now that we know what content the user is requesting, let’s use it to render the UI on the server. In order to render the router on the server, we’ll have to make some significant changes to our Express configuration. To start, we’ll need to import a few modules:
import
{
Provider
}
from
'react-redux'
import
{
compose
}
from
'redux'
import
{
renderToString
}
from
'react-dom/server'
import
{
StaticRouter
}
from
'react-router-dom'
We need the Provider
, a compose
function, the renderToString
function, and the StaticRouter
. On the server, the StaticRouter is used when we want to render our component tree to a string.
In order to generate an HTML response, there are three steps:
serverStore
.StaticRouter
.We create one function for each of these steps and compose them into a single function, the htmlResponse
:
const
htmlResponse
=
compose
(
buildHTMLPage
,
// Step 3
renderComponentsToHTML
,
// Step 2
makeClientStoreFrom
(
serverStore
)
// Step 1
)
In this composition, the makeClientStoreFrom(serverStore)
is a higher-order function. Initially, this function is invoked with the serverStore
once. It returns a function that will be invoked on every request. The returned function will always have access to the serverStore
.
When htmlResponse
is invoked, it expects a single argument: the url
that has been requested by the user. For step one, we will create a higher-order function that packages the url
with a new client store
created using the current state of the serverStore
. Both store
and url
are passed to the next function, step 2, in a single object:
const
makeClientStoreFrom
=
store
=>
url
=>
({
store
:
storeFactory
(
false
,
store
.
getState
()),
url
})
The output from the makeClientStoreFrom
function becomes the input for the renderComponentToHTML
function. This function expects that the url
and store have been packaged into a single argument:
const
renderComponentsToHTML
=
(
{
url
,
store
}
)
=>
(
{
state
:
store
.
getState
(
)
,
html
:
renderToString
(
<
Provider
store
=
{
store
}
>
<
StaticRouter
location
=
{
url
}
context
=
{
{
}
}
>
<
App
/
>
<
/
S
t
a
t
i
c
R
o
u
t
e
r
>
<
/
P
r
o
v
i
d
e
r
>
)
}
)
The renderComponentsToHTML
returns an object with two properties: state
and html
. The state
is obtained from the new client store and the html
is generated by the renderToString
method. Since the app still uses Redux in the browser, the Provider
is rendered as the root component, and the new client store is passed to it as a property.
The StaticRouter
component is used to render the UI based upon the location that is being requested. The StaticRouter
requires a location
and context
. The requested url
is passed to the location
property and an empty object is passed to context
. When these components are rendered to an HTML string, the location will be taken into account, and the StaticRouter
will render the correct routes.
This function returns the two necessary components to build the page: the current state of the organizer, and the UI rendered to an HTML string.
The state
and the html
can be used in the last composed function, buildHTMLPage
:
const
buildHTMLPage
=
(
{
html
,
state
}
)
=>
`
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Universal Color Organizer</title> </head> <body> <div id="react-container">
${
html
}
</div> <script>
window.__INITIAL_STATE__ =
${
JSON
.
stringify
(
state
)
}
</script> <script src="/bundle.js"></script> </body> </html>
`
Our color wall is now isomorphic. It will render the UI on the server and send it to the client as text. It will also embed the initial state of the store directly into the response.
The browser initially displays the UI obtained in the HTML response. When the bundle loads, it re-renders the UI and the client takes over from there. From this point on, all user interactivity including navigation will happen on the client. Our single page application will function as it always has, unless the browser is refreshed, at which point the server rendering process starts all over again.
Here is all of the current code from the Express app module, the entire file:
import
express
from
'express'
import
path
from
'path'
import
fs
from
'fs'
import
{
Provider
}
from
'react-redux'
import
{
compose
}
from
'redux'
import
{
StaticRouter
}
from
'react-router-dom'
import
{
renderToString
}
from
'react-dom/server'
import
App
from
'../components/App'
import
storeFactory
from
'../store'
import
initialState
from
'../../data/initialState.json'
const
fileAssets
=
express
.
static
(
path
.
join
(
__dirname
,
'../../dist/assets'
)
)
const
serverStore
=
storeFactory
(
true
,
initialState
)
serverStore
.
subscribe
(()
=>
fs
.
writeFile
(
path
.
join
(
__dirname
,
'../../data/initialState.json'
),
JSON
.
stringify
(
serverStore
.
getState
()),
error
=>
(
error
)
?
console
.
log
(
"Error saving state!"
,
error
)
:
null
)
)
const
logger
=
(
req
,
res
,
next
)
=>
{
console
.
log
(
`
${
req
.
method
}
request for '
${
req
.
url
}
'`
)
next
()
}
const
addStoreToRequestPipeline
=
(
req
,
res
,
next
)
=>
{
req
.
store
=
serverStore
next
()
}
const
makeClientStoreFrom
=
store
=>
url
=>
({
store
:
storeFactory
(
false
,
store
.
getState
()),
url
})
const
renderComponentsToHTML
=
({
url
,
store
})
=>
({
state
:
store
.
getState
(),
css
:
defaultStyles
,
html
:
renderToString
(
<
Provider
store
=
{
store
}
>
<
StaticRouter
location
=
{
url
}
context
=
{{}}
>
<
App
/>
<
/StaticRouter>
<
/Provider>
)
})
const
buildHTMLPage
=
({
html
,
state
})
=>
`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Universal Color Organizer</title>
</head>
<body>
<div id="react-container">
${
html
}
</div>
<script>
window.__INITIAL_STATE__ =
${
JSON
.
stringify
(
state
)
}
</script>
<script src="/bundle.js"></script>
</body>
</html>
`
const
htmlResponse
=
compose
(
buildHTMLPage
,
renderComponentsToHTML
,
makeClientStoreFrom
(
serverStore
)
)
const
respond
=
(
req
,
res
)
=>
res
.
status
(
200
).
send
(
htmlResponse
(
req
.
url
))
export
default
express
()
.
use
(
logger
)
.
use
(
fileAssets
)
.
use
(
addStoreToRequestPipeline
)
.
use
(
respond
)
Our app now allows users to bookmark URLs and send URLs to other users that will be rendered isomorphically. The router decides which content to render based upon the URL. It does so on the server, which means that our users can access our content rapidly.
Isomorphic applications have the best of both worlds: they can take advantage of the speediness, control, and security that server rendering provides, while benefiting from the low bandwidth and speed that is gained from single page applications. An isomorphic React application is essentially a server-rendered SPA, which lays the foundation for you to build efficient applications that will be cool but also fast and efficient.
At present, we are rendering the HTML on the server, but the CSS does not get rendered until the bundle is loaded in the browser. The result is a strange flicker. Initially we will see all of the unstyled content in the browser before the CSS is loaded. When JavaScript is turned off in the browser, users will not see any CSS styles at all because they are embedded in the JavaScript bundle.
The solution is to add the styles directly to the response. To do this, we must first extract the CSS from the webpack bundle into its own separate file. You will need to install the extract-text-webpack-plugin
:
npm install extract-text-webpack-plugin
You will also need to require this plugin in your webpack configuration:
var
webpack
=
require
(
"webpack"
)
var
ExtractTextPlugin
=
require
(
"extract-text-webpack-plugin"
)
var
OptimizeCss
=
require
(
'optimize-css-assets-webpack-plugin'
)
Also, in the webpack configuration, we need to replace the CSS and SCSS loaders with loaders that use the ExtractTextPlugin
:
{
test
:
/.css$/
,
loader
:
ExtractTextPlugin
.
extract
(
{
fallback
:
"style-loader"
,
use
:
[
"style-loader"
,
"css-loader"
,
{
loader
:
"postcss-loader"
,
options
:
{
plugins
:
(
)
=>
[
require
(
"autoprefixer"
)
]
}
}
]
}
)
}
,
{
test
:
/.scss/
,
loader
:
ExtractTextPlugin
.
extract
(
{
fallback
:
"style-loader"
,
use
:
[
"css-loader"
,
{
loader
:
"postcss-loader"
,
options
:
{
plugins
:
(
)
=>
[
require
(
"autoprefixer"
)
]
}
}
,
"sass-loader"
]
}
)
}
And we need to include that plugin in our configuration inside of the plugins array. Here, when the plugin is included, we specify the filename of the CSS file to extract:
plugins
:
[
new
ExtractTextPlugin
(
"bundle.css"
)
,
new
OptimizeCss
(
{
assetNameRegExp
:
/.optimize.css$/g
,
cssProcessor
:
require
(
'cssnano'
)
,
cssProcessorOptions
:
{
discardComments
:
{
removeAll
:
true
}
}
,
canPrint
:
true
}
)
]
Now when webpack runs, it will not include the CSS in the JavaScript bundle; it will instead extract all of the CSS into a separate file, ./assets/bundle.css.
We also need to modify the Express configuration. When the organizer starts, the CSS is saved as a global string. We can use the filesystem or fs
module to read the contents of a text file into the variable staticCSS
:
const
staticCSS
=
fs
.
readFileSync
(
path
.
join
(
__dirname
,
'../../dist/assets/bundle.css'
)
)
Now we have to modify the buildHTMLPage
function to write the CSS directly to the response inside of a <style>
tag:
const
buildHTMLPage
=
(
{
html
,
state
}
)
=>
`
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Universal Color Organizer</title>
<style>
${
staticCSS
}
</style>
</head> <body> <div id="react-container">
${
html
}
</div> <script> window.__INITIAL_STATE__ =
${
JSON
.
stringify
(
state
)
}
</script> <script src="/bundle.js"></script> </body> </html>
`
CSS is now directly embedded into our response. There is no longer a strange, styleless flicker. When JavaScript is turned off, the styles remain in place.
We now have an isomorphic color organizer that shares a lot of universal JavaScript. Initially, the color organizer is rendered on the server, but it is also rendered on the browser after the page is finished loading. When the browser takes over, the color organizer behaves as a single-page application.
At present, the color organizer is rendering UI on the server and re-rendering UI in the browser. Once the browser takes over, the organizer functions as a single-page application. Users dispatch actions locally, the local state changes, and locally the UI is updated. Everything is working well in the browser, but the dispatched actions are not making it back to the server.
In this next section, we will not only make sure that this data gets saved on the server, we will make sure that the action objects themselves are created on the server and dispatched to both stores.
In the color organizer, we will integrate a REST API for handling our data. Actions will be initiated on the client, completed on the server, and then dispatched to both stores. The serverStore
will save the new state to JSON, and the client store will trigger a UI update. Both stores will dispatch the same actions universally (#fig1202).
Let’s take a look at an example of the complete process for dispatching an ADD_COLOR
action in the proposed solution:
Dispatch action creator addColor()
with new title and color.
Send data to server in new POST request.
Create and dispatch the new ADD_COLOR
add color action on the server.
Send the ADD_COLOR
action in the response body.
Parse the response body and dispatch the ADD_COLOR
action on the client.
The first thing that we need to do is build the REST API. Let’s create a new file called ./src/server/color-api.js.
Every action created is handled the same way: it is dispatched on the server and then it is sent to the client. Let’s create a function that dispatches the action to the serverStore
and sends the action to the client using the response object:
const
dispatchAndRespond
=
(
req
,
res
,
action
)
=>
{
req
.
store
.
dispatch
(
action
)
res
.
status
(
200
).
json
(
action
)
}
Once we have an action, we can use this function to dispatch the action and send a response to the client.
We will need to create some HTTP endpoints using the Express Router
that can handle various HTTP requests. We will create routes to handle GET
, POST
, PUT
, and DELETE
requests on the route /api/colors. The Express Router
can be used to create these routes. Each route will contain the logic to create a different action object and send it to the dispatchAndRespond
function along with the request and response objects:
import
{
Router
}
from
'express'
import
{
v4
}
from
'uuid'
const
dispatchAndRespond
=
(
req
,
res
,
action
)
=>
{
req
.
store
.
dispatch
(
action
)
res
.
status
(
200
)
.
json
(
action
)
}
const
router
=
Router
(
)
router
.
get
(
"/colors"
,
(
req
,
res
)
=>
res
.
status
(
200
)
.
json
(
req
.
store
.
getState
(
)
.
colors
)
)
router
.
post
(
"/colors"
,
(
req
,
res
)
=>
dispatchAndRespond
(
req
,
res
,
{
type
:
"ADD_COLOR"
,
id
:
v4
(
)
,
title
:
req
.
body
.
title
,
color
:
req
.
body
.
color
,
timestamp
:
new
Date
(
)
.
toString
(
)
}
)
)
router
.
put
(
"/color/:id"
,
(
req
,
res
)
=>
dispatchAndRespond
(
req
,
res
,
{
type
:
"RATE_COLOR"
,
id
:
req
.
params
.
id
,
rating
:
parseInt
(
req
.
body
.
rating
)
}
)
)
router
.
delete
(
"/color/:id"
,
(
req
,
res
)
=>
dispatchAndRespond
(
req
,
res
,
{
type
:
"REMOVE_COLOR"
,
id
:
req
.
params
.
id
}
)
)
export
default
router
Each function added to the router object handles a different request for http://localhost:3000/api/{route}:
GET '/colors'
Responds with the current color array from the server’s state. This route is added just so we can see the listed colors; it is not used by the frontend.
POST '/colors'
Creates a new color action object and sends it to dispatchAndRespond
.
PUT '/color/:id'
Changes the rating of a color. The color’s ID is obtained from route paramaters and used in the new action object.
DELETE '/color/:id'
Removes a color based upon the ID sent in the routing parameters.
Now that we have defined the routes, we need to add them to the Express app configuration. First, we install the Express body-parser
:
npm install body-parser --save
The body-parser
is used to parse incoming request bodies and obtain any variables sent to the routes. It is necessary to obtain the new color and rating information from the client. We’ll need to add this middleware to our Express app configuration. Let’s import the body-parser
and our new routes into the ./server/app.js file:
import
bodyParser
from
'body-parser'
import
api
from
'./color-api'
Let’s add the bodyParser
middleware and the API to our Express app. It is important to add the bodyParser
before the API so that the data can be parsed by the time the request has been handled by the API:
export
default
express
(
)
.
use
(
logger
)
.
use
(
fileAssets
)
.
use
(
bodyParser
.
json
(
)
)
.
use
(
addStoreToRequestPipeline
)
.
use
(
'/api'
,
api
)
.
use
(
matchRoutes
)
The bodyParser.json()
is now parsing incoming request bodies that have been formatted as JSON. Our color-api
is added to the pipeline and configured to respond to any routes that are prefixed with /api. For example, this URL can be used to obtain the current array of colors as JSON: http://localhost:3000/api/colors.
Now that our Express app has endpoints that can respond to HTTP requests, we are ready to modify the frontend action creators to communicate with these endpoints.
One problem with client/server communication is latency, or the delay that we experience while waiting for a response after sending a request. Our action creators need to wait for a response before they can dispatch the action, because in our solution the action itself is being sent to the client from the server. There is middleware for Redux that can help us with asynchronous actions: it is called redux-thunk
.
In this next section, we will rewrite out action creators using redux-thunk
. These action creators, called thunks, will allow us to wait for a server response before dispatching an action locally. Thunks are higher-order functions. Instead of action objects, they return other functions. Let’s install redux-thunk
:
npm install redux-thunk --save
redux-thunk
is middleware; it needs to be incorporated into our storeFactory
. First, at the top of ./src/store/index.js, import redux-thunk
:
import
thunk
from
'redux-thunk'
The storeFactory
has a function called middleware. It returns the middleware that should be incorporated to the new store in a single array. We can add any Redux middleware to this array. Each item will be spread into the arguments of the applyMiddleware
function:
const
middleware
=
server
=>
[
(
server
)
?
serverLogger
:
clientLogger
,
thunk
]
const
storeFactory
=
(
server
=
false
,
initialState
=
{
}
)
=>
applyMiddleware
(
...
middleware
(
server
)
)
(
createStore
)
(
combineReducers
(
{
colors
}
)
,
initialState
)
export
default
storeFactory
Let’s take a look at the current action creator for adding colors:
export
const
addColor
=
(
title
,
color
)
=>
({
type
:
"ADD_COLOR"
,
id
:
v4
(),
title
,
color
,
timestamp
:
new
Date
().
toString
()
})
...
store
.
dispatch
(
addColor
(
"jet"
,
"#000000"
))
This action creator returns an object, the addColor
action. That object is immediately dispatched to the store. Now let’s look at the thunk version of addColor
:
export
const
addColor
=
(
title
,
color
)
=>
(
dispatch
,
getState
)
=>
{
setTimeout
(
(
)
=>
dispatch
(
{
type
:
"ADD_COLOR"
,
index
:
getState
(
)
.
colors
.
length
+
1
,
timestamp
:
new
Date
(
)
.
toString
(
)
title
,
color
}
)
,
2000
)
}
...
store
.
dispatch
(
addColor
(
"jet"
,
"#000000"
)
)
Even though both action creators are dispatched the exact same way, the thunk returns a function instead of an action. The returned function is a callback that receives the store’s dispatch
and getState
methods as arguments. We can dispatch an action when we are ready. In this example, a setTimeout
is used to create a two-second delay before we dispatch a new color action.
In addition to dispatch
, thunks also have access to the store’s getState
method. In this example, we used it to create an index field based upon the current number of colors in state. This function can be useful when it is time to create actions that depend upon data from the store.
Not all of your action creators have to be thunks. The redux-thunk
middleware knows the difference between thunks and action objects. Action objects are immediately dispatched.
Thunks have another benefit. They can invoke dispatch
or getState
asynchronously as many times as they like, and they are not limited to dispatching one type of action. In this next sample, the thunk immediately dispatches a RANDOM_RATING_STARTED
action and repeatedly dispatches a RATE_COLOR
action that rates a specific color at random:
export
const
rateColor
=
id
=>
(
dispatch
,
getState
)
=>
{
dispatch
(
{
type
:
"RANDOM_RATING_STARTED"
}
)
setInterval
(
(
)
=>
dispatch
(
{
type
:
"RATE_COLOR"
,
id
,
rating
:
Math
.
floor
(
Math
.
random
(
)
*
5
)
}
)
,
1000
)
}
...
store
.
dispatch
(
rateColor
(
"f9005b4e-975e-433d-a646-79df172e1dbb"
)
)
These thunks are simply samples. Let’s build the real thunks that the color organizer will use by replacing our current action creators.
First, we’ll create a function called fetchThenDispatch
. This function uses isomorphic-fetch
to send a request to a web service and automatically dispatch the response:
import
fetch
from
'isomorphic-fetch'
const
parseResponse
=
response
=>
response
.
json
()
const
logError
=
error
=>
console
.
error
(
error
)
const
fetchThenDispatch
=
(
dispatch
,
url
,
method
,
body
)
=>
fetch
(
url
,
{
method
,
body
,
headers
:
{
'Content-Type'
:
'application/json'
}
}
).
then
(
parseResponse
)
.
then
(
dispatch
)
.
catch
(
logError
)
The fetchThenDispatch
function requires the dispatch
function, a URL, the HTTP request method, and the HTTP request body as arguments. This information is then used in the fetch
function. Once a response is received, it will be parsed and then dispatched. Any errors will be logged to the console.
We’ll use the fetchThenDispatch
function to help us construct thunks. Each thunk will send a request to our API, along with any necessary data. Since our API responds with action objects, the response can be immediately parsed and dispatched:
export
const
addColor
=
(
title
,
color
)
=>
dispatch
=>
fetchThenDispatch
(
dispatch
,
'/api/colors'
,
'POST'
,
JSON
.
stringify
({
title
,
color
})
)
export
const
removeColor
=
id
=>
dispatch
=>
fetchThenDispatch
(
dispatch
,
`/api/color/
${
id
}
`
,
'DELETE'
)
export
const
rateColor
=
(
id
,
rating
)
=>
dispatch
=>
fetchThenDispatch
(
dispatch
,
`/api/color/
${
id
}
`
,
'PUT'
,
JSON
.
stringify
({
rating
})
)
The addColor
thunk sends a POST request to http://localhost:3000/api/colors along with the title and hex value of the new color. An ADD_COLOR
action object is returned, parsed, and dispatched.
The removeColor
thunk sends a DELETE request to the API with the ID of the color to delete provided in the URL. A REMOVE_COLOR
action object is returned, parsed, and dispatched.
The rateColor
thunk sends a PUT request to the API. The ID of the color to rate is included in the URL as a route parameter, and the new rating is supplied in the body of the request. A RATE_COLOR
action object is returned from the server, parsed as JSON, and dispatched to the local store.
Now when you run the application, you can see actions being dispatched to both stores in the console log. The browser console is a part of the developer tools and the server console is the terminal where the server was started (Figure 12-3).
Just about every React application that you build will require the existence of some type of web server. Sometimes you will only need a web server to host your application. Other situations require communications with web services. And then there are high-traffic applications that need to work on many platforms that will require different solutions entirely.
The key to React development is knowing when to use the right tools. You already have many of the tools needed to build robust applications in your toolbelt. Only use what is needed. If your application does not depend upon a lot of data, don’t use Redux. React state is a great solution that is perfect fit for the right size app. Your application may not require server rendering. Don’t worry about incorporating it until you have a highly interactive app that has a lot of mobile traffic.
When setting forth to develop your own React applications, we hope that this book will serve as a reference and a great foundation. Though React and its related libraries will almost certainly go through changes, these are stable tools that you can feel confident about using right away. Building apps with React, Redux, and functional, declarative JavaScript is a lot of fun, and we can’t wait to see what you’ll build.
1 Gert Hengeveld, “Isomorphism vs Universal JavaScript”, Medium.
2 Express Documentation, “Using Middleware”.
3 Andrew H. Farmer, “Should I use React Server-Side Rendering?”