React provides two methodsrenderToStringandrenderToStaticMarkupis used to output the component (Virtual DOM) as an HTML string, which is the basis of React server-side rendering, which removes the server-side dependency on the browser environment, making server-side rendering an attractive thing.
Server-Side rendering in addition to resolving the dependency on the browser environment, there are two issues to solve:
- The front and back end can share code
- Front-end routing can be processed uniformly
React Ecology provides a lot of options, here we use Redux and react-router to do the explanation.
Redux
Redux provides a one-way, flux-like data stream that maintains only one store for the entire application and a function-oriented feature that makes it friendly to server-side rendering support.
2 minutes to see how Redux works.
About Store:
- The entire app has only one store
- The state tree corresponding to the Store, generated by invoking a reducer function (root reducer)
- Each field on the state tree can be further generated by a different reducer function
- The Store contains several methods, for exampledispatch,getStateto handle data flow
- The Store's state tree can only be triggered by thedispatch(action)change
Redux Data Flow:
- Action is a { PAYLOAD  Object
- reducer function through store.dispatch ( action) triggers the
- reducer function to accept ( state, action) Two parameters, returns a new state
- reducer function to judge action.< Span class= "Hljs-typedef" >type then handles the corresponding action.payload Data to update the status tree
So for the entire application, a store corresponds to a UI snapshot, and server-side rendering simplifies the server-side initialization of the store, passes the store into the app's root component, andrenderToStringoutputs the entire app to the HTML that contains the initialized data for the root component invocation.
React-router
React-router matches different routes in a declarative way, decides to show different components on the page, and passes the routing information to the component using props, so as long as the routing changes, props changes, triggering the component Re-render.
Suppose there is a very simple application, with only two pages, a list page/listand a detail page/item/:id, click on the entry on the list to enter the details page.
You can define routes like this,./routes.js
import React from ‘react’;
import {Route} from ‘react-router’;
import {List, Item} from ‘./components’;
// stateless component, a simple container, react-router will
// The components matched by the rule are passed as `props.children`
const Container = (props) => {
return (
<div> {props.children} </ div>
);
};
// route rule:
//-`/ list` shows` List` component
//-`/ item /: id` displays the` Item` component
const routes = (
<Route path = "/" component = {Container}>
<Route path = "list" component = {List} />
<Route path = "item /: id" component = {Item} />
</ Route>
);
export default routes;
Starting from here, we explain some of the details involved in implementing server-side rendering with this very simple application.
Reducer
The store is produced by Reducer, so reducer actually reflects the state tree structure of the store
./reducers/index.js
import listReducer from ‘./list‘;
import itemReducer from ‘./item‘;
export default function rootReducer(state = {}, action) {
return { list: listReducer(state.list, action), item: itemReducer(state.item, action)
};
}
rootReducerParameters are thestateentire Store's state tree, and each field under the state tree can have its own
Reducer, so introduced herelistReduceranditemReducer, you can see these two reducer
The state parameter is just the corresponding and field on the entire status treelistitem.
Specific to./reducers/list.js
const initialState = [];
export default function listReducer(state = initialState, action) {
switch(action.type) {
case ‘FETCH_LIST_SUCCESS‘: return [...action.payload];
default: return state;
}
}
A list is a simple array that contains items, which may resemble this structure:[{ id: 0, name: ‘first item‘}, {id: 1, name: ‘second item‘}]‘FETCH_LIST_SUCCESS‘obtained fromaction.payload.
Then./reducers/item.js, process the retrieved item data.
const initialState = {};
export default function listReducer(state = initialState, action) {
switch(action.type) {
case ‘FETCH_ITEM_SUCCESS‘: return [...action.payload];
default: return state;
}
}
Action
The corresponding should have two action to get list and item, trigger reducer change Store, here we definefetchListandfetchItemtwo action.
./actions/index.js
import fetch from ‘isomorphic-fetch‘;
export function fetchList() {
return (dispatch) => { return fetch(‘/api/list‘) .then(res => res.json()) .then(json => dispatch({ type: ‘FETCH_LIST_SUCCESS‘, payload: json }));
}
}
export function fetchItem(id) {
return (dispatch) => { if (!id) return Promise.resolve(); return fetch(`/api/item/${id}`) .then(res => res.json()) .then(json => dispatch({ type: ‘FETCH_ITEM_SUCCESS‘, payload: json }));
}
}
Isomorphic-fetch is a common Ajax implementation of the front and back end, it is important to share the code back and forth.
In addition, because it involves asynchronous requests, here the action uses thunk, that is, the function, redux throughthunk-middlewareto handle this kind of action, the function as a normal action dispatch, such asdispatch(fetchList())
Store
We use a standalone./store.js, configuration (such as Apply middleware) to generate the Store
import { createStore } from ‘redux‘;
import rootReducer from ‘./reducers‘;
// Apply middleware here
// ...
export default function configureStore(initialState) {
const store = createStore(rootReducer, initialState);
return store;
}
React-redux
Next implement<List>the<Item>component, then associate the Redux and react components, see React-redux for details.
./app.js
import React from ‘react’;
import {render} from ‘react-dom’;
import {Router} from ‘react-router’;
import createBrowserHistory from ‘history / lib / createBrowserHistory’;
import {Provider} from ‘react-redux’;
import routes from ‘./routes’;
import configureStore from ‘./store’;
// `__INITIAL_STATE__` comes from server-side rendering, the next section details
const initialState = window .__ INITIAL_STATE__;
const store = configureStore (initialState);
const Root = (props) => {
return (
<div>
<Provider store = (store}>
<Router history = (createBrowserHistory ())>
{routes}
</ Router>
</ Provider>
</ div>
);
}
render (<Root />, document.getElementById (‘root’));
At this point, the client section ends.
Server Rendering
The next server side is relatively simple, get data can call Action,routes on the server side of the processing reference React-router server rendering, on the server side with amatchmethod will get the request URL Match to the routes that we defined earlier, parsed into a client-consistent props object passed to the component.
./server.js
import express from ‘express‘;
import React from ‘react‘;
import { renderToString } from ‘react-dom/server‘;
import { RoutingContext, match } from ‘react-router‘;
import { Provider } from ‘react-redux‘;
import routes from ‘./routes‘;
import configureStore from ‘./store‘;
const app = express();
function renderFullPage(html, initialState) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="root">
<div>
${html}
</div>
</div>
<script>
window.__INITIAL_STATE__ = ${JSON.stringify(initialState)};
</script>
<script src="/static/bundle.js"></script>
</body>
</html>
`;
}
app.use((req, res) => {
match({ routes, location: req.url }, (err, redirectLocation, renderProps) => {
if (err) {
res.status(500).end(`Internal Server Error ${err}`);
} else if (redirectLocation) {
res.redirect(redirectLocation.pathname + redirectLocation.search);
} else if (renderProps) {
const store = configureStore();
const state = store.getState();
Promise.all([
store.dispatch(fetchList()),
store.dispatch(fetchItem(renderProps.params.id))
])
.then(() => {
const html = renderToString(
<Provider store={store}>
<RoutingContext {...renderProps} />
</Provider>
);
res.end(renderFullPage(html, store.getState()));
});
} else {
res.status(404).end(‘Not found‘);
}
});
});
The server-side rendering section canstore.dispatch(action)obtain the Store data uniformly through the pooled client. Also note that therenderFullPagegenerated page HTML is in the part of the React component Mount (<div id="root">), and the HTML structure of the front and back ends should be consistent.storethe state tree is then written to a global variable (__INITIAL_STATE__) so that the client initializes render with the ability to validate the HTML structure generated by the server and synchronize to the initialization state, and then the entire page is taken over by the client.
Finally about how to deal with the link jump in the page?
React-router provides a<Link>component to replace the<a>label, which manages the browser history so that it does not request the server every time a click is made, and can then be processed by bindingonClickevents.
For example/list, on the page, for each item you<Link>bind a route URL:/item/:id, and bindonClickto triggerdispatch(fetchItem(id))fetch data, display the details page content.
More references
- Universal (Isomorphic)
- Isomorphic-redux-app
Play React server-side rendering