preface

This article aims to help readers build their own Server Side Render app that meets SEO requirements under React Router V4.

react

Not to mention, in recent years, rapid development, is widely used by major mainstream applications;

react-router-v4

The React route has received a major overhaul. If your app is 16 years old or older, you’ll still be using v3, but that’s okay. Try upgrading your router now, as shown in this article.

Compared to V3, V4 has the following major changes:

  • Declarative routing;
  • Distributed routing without centralized configuration;
  • Routing supports regular matching. Match rule
  • For Web apps and React Native apps, it is a separate package.
  • Instead of using the onEnter and onChange callbacks, you need to use component’s lifecycle function to do the same thing.

This article assumes that you are already familiar with React, Redux, and Express, and have built your own app.

Set up

1. Install React Router V4.

npm i --save react-router-dom react-router-config
Copy the code

React-router-config is the web router, which further encapsulates the React-Router (native app installation package is react-router-native) and retains some of the react-Router interfaces. If you install a react-router-config server on your system, use react-router-config instead.

2. Configure routes.

If you are using V3, you may have a centralized routing file like this:

import React from 'react';
import { Route } from 'react-router';

const Routes = () => (
  <Route path="/" onEnter={() => {}} onChange={() => {}}>
    <Route path=":channel/abc" component={MyContainer1} />
    <Route path=":channel/def" component={MyContainer2} />
    <Route path=":channel/*" component={NotFoundContainer} status={404} />
  </Route>
);

export default Routes;
Copy the code

V4 is a very different approach. First, define a route configuration for you:

# routes.js
import RootApp from './RootApp';
import Home from './Home';
import List from './List';

const routes = [
  { component: RootApp,
    routes: [ # multilevel nesting
      { path: '/',
        exact: true,
        component: Home
      },
      { path: '/home',
        component: Home
      },
      { path: '/list',
        component: List
      }
    ]
  }
];

export default routes;
Copy the code

On your client, use

render your routes. Your client.js looks something like this:

# client.js
import React from 'react';
import {render} from 'react-dom';
import BrowserRouter from 'react-router-dom/BrowserRouter';
import { renderRoutes } from 'react-router-config';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import routes from './routes'; # routes configuration defined above
import reducers from './modules';

const store = createStore(
  reducers, window.__INITIAL_STATE__, applyMiddleware(thunk) 
);

const AppRouter = () => {
  return (
    <Provider store={store}>
      <BrowserRouter>  # use v4 BrowserRouter;
        {renderRoutes(routes)} Use v4 renderRoutes;
      </BrowserRouter>
    </Provider>
  )
}

render(<AppRouter />, document.querySelector('#app'));
Copy the code

On your server, use

, eg.server.js:

# server.js
import express from 'express';
import request from 'request';
import React from 'react';
import { renderToString } from 'react-dom/server';
import StaticRouter from 'react-router-dom/StaticRouter';
import { renderRoutes } from 'react-router-config';
import routes from './routes'; # routes configuration defined above

const router = express.Router();

router.get(The '*', (req, res) => {
  let context = {};
  const content = renderToString(
    <StaticRouter location={req.url} context={context}>
      {renderRoutes(routes)}
    </StaticRouter>
  );
  res.render('index', {title: 'Express', data: false, content });
});

module.exports = router;
Copy the code

Implement your root Component:

# RootApp.js
import React from "react";
import { renderRoutes } from "react-router-config";

const RootApp = (props) => {
  return (
    <div>
      {renderRoutes(props.route.routes)} # distributed, if you use nested routines, render similar on each of your father components;
    </div>
  );
};

export default RootApp;
Copy the code

RenderRoutes does things like this for you:

render() {
  return(
    <Switch>
      <Route exact path="/" component={Home}/>
      <Route path="/home" component={Home}/>
      <Route path="/list" component={List}/>
    </Switch>
  )
}
Copy the code

3. The fetch data

If your app needs to fetch data, you can use the following methods under V4:

Centralized:

Configure the fetch action in your route configuration and handle it in server.js:

# routes.js
import RootApp from './RootApp';
import Home from './Home';
import List from './List'; Import {fetchRootData, fetchHomeData, fetchListData} from'./fetchData';

const routes = [
  { component: RootApp,
    fetchData: () => {fetchRootData}
    routes: [
      { path: '/',
        exact: true, Component: Home, fetchData: () => {fetchHomeData}}, {path:'/home',
        component: Home,
        fetchData: () => {fetchHomeData}
      },
      { path: '/list',
        component: List,
        fetchData: () => {fetchListData}
      }
    ]
  }
];

export default routes;
Copy the code
# server.js
import express from 'express';
import request from 'request';
import React from 'react';
import { renderToString } from 'react-dom/server';
import StaticRouter from 'react-router-dom/StaticRouter';
import { renderRoutes } from 'react-router-config';
import routes from './routes'; # routes configuration defined above

const router = express.Router();

router.get(The '*', (req, res) => {
  const {path, query} = req;
  const matchedRoutes = matchRoutes(routes, path); Req. url contains a query. V4 router does not filter the query when performing a regular match.
  const store = configureStore();
  const dispatch = store.dispatch;
  
  const promises = matchedRoutes.map(({route}) => {
    let fetchData = route.fetchData;
    return fetchData instanceof Function ? fetchData(store) : Promise.resolve(null)
  });
  return Promise.all(promises)
    .then(throwErrorIfApiResponseFailed(store)) # implement yourself
    .then(handleSuccessPage(store, req, res)) # server side <StaticRouter> render, implement yourself
    .catch(handleError(res, query)); # error handler, return error page and error code, implement yourself
});

module.exports = router;
Copy the code

This is a centralized configuration, centralized processing, but one of the things that needs to be noticed is how the client side fetchData, V4 no longer directly supports history.listen (which intercepts and appends a series of operations when a client URL changes), so you need to find a way to get data to the client side as well.

Distributed:

Use the React lifecycle function to trigger Action requests at appropriate times, scattering fetch data across each component level.

#
componentDidMount() {
    fetchData();
}
Copy the code

You may be wondering, this is only for client, what about server side? You can bind the fetchData Action to the Component like this, and then the Server side can handle it as well:

# RootApp
class List extends Component {
  static fetchData(store) {
    return store.dispatch(fetchUsers());
  }

  componentDidMount() {
    this.props.fetchUsers();
  }
 render() {
   return <div></div>
 }
}
Copy the code
# server.js
import express from 'express';
import request from 'request';
import React from 'react';
import { renderToString } from 'react-dom/server';
import StaticRouter from 'react-router-dom/StaticRouter';
import { renderRoutes } from 'react-router-config';
import routes from './routes'; # routes configuration defined above

const router = express.Router();

router.get(The '*', (req, res) => {
  const {path, query} = req;
  const matchedRoutes = matchRoutes(routes, path); 
  const store = configureStore();
  const dispatch = store.dispatch;
  
  const promises = matchedRoutes.map(({route}) => {
    let fetchData = route.component.fetchData;
    return fetchData instanceof Function ? fetchData(store) : Promise.resolve(null)
  });
  return Promise.all(promises) # server side centralizes request data;
    .then(throwErrorIfApiResponseFailed(store))
    .then(handleSuccessPage(store, req, res)) # server side <StaticRouter> render, implement yourself
    .catch(handleError(res, query)); # error handler, return error page and error code, implement yourself
});

module.exports = router;

Copy the code

5. Handle 404 error page

# routes.js
import RootApp from './RootApp';
import Home from './Home';
import List from './List';

const routes = [
  { component: RootApp,
    routes: [
      { path: '/',
        exact: true,
        component: Home
      },
      { path: '/home',
        component: Home
      },
      { path: '/list',
        component: List
      },
      {
+       path: The '*',
+       component: NotFound
      }
    ]
  }
];

export default routes;
Copy the code

Your NotFound Component maintains the error code itself;

# Notfound.js
import React from 'react';
import { Route } from 'react-router-dom';

const NotFound = () => {
  return (
    <Route render={({ staticContext }) => {
      if (staticContext) {
        staticContext.status = 404;
      }
      return (
        <div>
          <h1>404 : Not Found</h1>
        </div>
      )
    }}/>
  );
};

export default NotFound;
Copy the code

Server side:

# server.js
import express from 'express';
import request from 'request';
import React from 'react';
import { renderToString } from 'react-dom/server';
import StaticRouter from 'react-router-dom/StaticRouter';
import { renderRoutes } from 'react-router-config';
import routes from './routes'; # routes configuration defined above

const router = express.Router();

router.get(The '*', (req, res) => {
  let context = {};
  const content = renderToString(
    <StaticRouter location={req.url} context={context}>
      {renderRoutes(routes)}
    </StaticRouter>
  );
+ if(context.status === 404) { Get the status code and respond;
+     res.status(404);
+ }
  res.render('index', {title: 'Express', data: false, content });
});

module.exports = router;
Copy the code

5. Handle redirects

# routes.js
import AppRoot from './AppRoot';
import Home from './Home';
import List from './List';
import NotFound from './Notfound';
+import ListToUsers from './ListToUsers';

const routes = [
  { component: AppRoot,
    routes: [
     { path: '/',
        exact: true,
        component: Home
      },
      { path: '/home',
        component: Home
      },
+     { path: '/list',
+       component: ListToUsers
+     }
+     { path: '/users',
+       component: List
+     }
      {
        path: The '*',
        component: NotFound
      }
    ]
  }
];

export default routes;
Copy the code

Like 404, the Component maintains its own status code:

# ListToUsers.jsx
import React from 'react';
import { Route, Redirect } from 'react-router-dom';

const ListToUsers = () => {
  return (
    <Route render={({ staticContext }) => {
      if (staticContext) {
        staticContext.status = 302;
      }
      return <Redirect from="/list" to="/users" /> # react redirect}} / >); };export default ListToUsers;
Copy the code

Server side:

# server.js
import express from 'express';
import request from 'request';
import React from 'react';
import { renderToString } from 'react-dom/server';
import StaticRouter from 'react-router-dom/StaticRouter';
import { renderRoutes } from 'react-router-config';
import routes from './routes'; # routes configuration defined above

const router = express.Router();

router.get(The '*', (req, res) => {
  let context = {};
  const content = renderToString(
    <StaticRouter location={req.url} context={context}>
      {renderRoutes(routes)}
    </StaticRouter>
  );
  if(context.status === 404) {
      res.status(404);
  }
+ if (context.status === 302) { Get the status code and respond;
+     return res.redirect(302, context.url);
+   }
  res.render('index', {title: 'Express', data: false, content });
});

module.exports = router;

Copy the code

React-router-v4 server side render server side render server side render V4 gives you more flexibility with multiple pages.