Reactjs SSR Tips and Tricks

Ahmad Tahani
7 min readJul 27, 2018

--

Recently I developed a Reactjs project base on the SSR technique, I’ve learned a lot. Now I want to consider some of the blind points in Reactjs SSR.

Maybe you’ve already known. But I think it would be useful to the developers who want to try SSR technique in Reactjs projects.

At first point, you need to know about what difference between client-side rendering and server-side rendering.

CSR vs. SSR

On regular SPA applications, we import the JS bundle files to the empty HTML file as script tags.

We have these steps on initial page load.

  1. The user types the web page URL and press enter.
  2. After some DNS manner, the GET request reaches to the server
  3. The server sends an HTML file as response.
  4. The browser is parsing the HTML and sees oh some external JS scripts should be downloaded.
  5. After waiting to download JS files. The browser parses and runs them.
  6. The root component is mounted it, and the componentDidMount event would be triggered, an async request run to get the data from the API.
  7. The data get from the server, and that component will be re-rendered to show the new data.

As you can see the data fetching and binding handle on the client side, We called it CSR ( Client Side Rendering ).
On the other hands, we have SSR ( Server-Side Rendering ) with these steps. For better understanding, You can see this sequence diagram.

Server-Side Rendering ( SSR )

As you can see the user would be able to see the whole web page before the JS bundle files are downloaded.

We can use defer attribute for JS scripts since we have a completed HTML file with data and we don’t need to execute scripts as soon as possible.

// You can find this line on server.js - razzle base project
<script src="${assets.client.js}" defer></script>

Which boilerplate should I use?

You could pick either next.js or react-server or Razzle. At first, I recommend seeing their documents. I found Razzle much simpler.
Razzle is a tool to abstract all of the complex configurations that needed for SSR into a single dependency. You are not limited to Reactjs; you also can use it with Reason, Elm, Vue, Angular as well.
At first, you start the project with zero configuration. Later you would be able to customize all of the settings according to your needs. You can even extend the Webpack configs easily.

How can we reach to initial method on the server side?

In Reactjs we are thinking in components. On CSR we always call the initial method on the componentDidMount event. So we need to access the initial method on server-side base on the route.
It’s a little complicated. Let ‘s see in action.

We usually define routes base on path and components like this. We could use the Route component everywhere.

const MainApp = () => (
<Switch>
<Route exact path="/" component={App} />
<Route path="/login" component={Authorize} />
</Switch>
);

There is a problem on SSR since we want to access to the component base on URL on the server. So we need to define all of the routes in one place in this way.

const routes = [
{
path: "/",
component: App,
initialMethod: () => getSomeData(),
routes: [
{
path: "/",
component: Dashboard,
exact: true,
initialMethod: () => getDashboardData(),
},
{
path: "/notifications",
component: Notifications,
beAuthorized: true,
initialMethod: () => getLatestNotifications(),
},
{
path: "/settings",
component: Settings,
beAuthorized: true
routes: [
{
path: "/settings/profile",
component: ProfileSettings,
initialMethod: () => getUserProfile(),
}
],
}
]
},
// other root routes like /login
];

We could define the initialMethod here or inside the Component as a method. You should note this method must be a Promise.

Now we should render routes base on Route component. At first, we need a Custom Route component to handle custom routes and beAuthorized attributes.

import React from "react";
import { Route, Redirect } from "react-router-dom";
const CustomRoute = ({
component: Component,
cmpProps = {},
routes,
exact = false,
path,
beAuthorized = false
}: Props) => {
// detect is user logged in or not
const isUserLoggedIn = getState().user;
// redirect the guest user on the protected path
if (beAuthorized && !isUserLoggedIn) {
return <Redirect to="/login" />;
}
return (
<Route
path={path}
exact={exact}
render={props =>
<Component
{...props}
{...cmpProps}
routes={routes}
/> }
/>
);
};

We passed routes attribute to the Component. For example, on Settings components, we could reach to routes via routes props.

We also define a component to iterate through the routes.

import React from "react";
import { Switch } from "react-router-dom";
import CustomRoute from "../CustomRoute";
const Routes = ({ routes, switchProps = {} }: Props) => (
<Switch {...switchProps}>
{routes.map((r, i) => (
<CustomRoute
key={r.key || i}
path={r.path}
component={r.component}
exact={r.exact}
beAuthorized={r.beAuthorized}
routes={r.routes}
/>
))}
</Switch>
);

On the server we could get the initialMethod base on match route. We have a helper function called matchPath, for more information see this document.

import { matchRoutes } from 'react-router-config'// inside a request /../ 
// router.get("/*",(res, req){ /.../ })
const branch = matchRoutes(routes, location.pathname)const promises = branch.map(({ route, match }) => {
return route.initialMethod
? route.initialMethod(match)
: Promise.resolve(null)
})
Promise.all(promises).then(data => {
// now you can send the rendered view to user
})

We are using Promise.all() to wait until all of the initialMethod has been resolved.

For example, if the request URL is / the Dashboard component should be rendered. In this case, we the two initialMethod to perform as Promise, getSomeData() and getDashboardData(). Could be two async API request, so you should notice to the performance issues.

The best way to test server-side fetching is disabling the JavaScript, how we can do that?

1- Go to Chrome Developer Tools

2- Go to Settings ( Press F1 )

3- Check the Disable JavaScript from Debugger section

You can test it on the Producthunt website.

The user can see the content even when JS is disabled.

We demonstrate it in a simple use case; In real-world applications, you would use redux-thunk or redux-saga and also you may need to pass some parameters to initialMethod. The redux-thunk implementation is much more the same, but the redux-saga is a little different.

Where Should I Store Persist Data Like Authentication Token?

On regular CSR web apps, you can save persist data on the local storage or IndexedDB. But on SSR we can’t access to these data like the authentication token in this way. So we should change our approach to Cookie.

For authentication, we have this scenario:

  1. API on the client side authorizes the user.
  2. The authentication data like access token has been saved to a cookie (browser cookie, tmp_user_atuh).
  3. On the server-side, we can access to the cookie, Now we can send requests to the protected endpoints.
  4. For better security, we save the cookie by the server. ( since the httpOnly option is set, nobody can modify it on the browser, user_auth).
Authentication Flow SSR

Our single source of truth is Redux store. Maybe the access token had been expired before SSR so at first point must refresh the access token then process the other API requests. We should always assume the Redux state as the single source of truth and check it if the cookie is not equal to the current state, we should update the value of the cookie.

How can we access preloaded state on the client side?

Through the data fetching on server-side, we have some bunch of data that bind to the components. On the client side we need these data as well, for example, our component has been connected to the redux store. So redux store should have these data as the initial state. We use window global object to pass redux store to the client side.

<script>
window.__INITIAL_STATE__ = ${serialize(preloadedState)};
</script>

How can we take apart two environments in data rendering?

On server-side we don’t have window object, for example, you may want to get window.location on your application, so you need to take apart two environments from each other. the easy way is you assume a state in the redux store like app.isSSR, on the server side should be set is as true, on the client should be set it as false, with this trick you would be able to pass current location on the server with a state like app.serverPath , So if you want to access current pathname, you could do like this.

const mapStateToProps = state  => ({
pathname: state.app.isSSR ?
state.app.serverPath :
window.location.pathname,
});

Caveats

  1. SSR is not suitable for any project, for example, SSR is not a good fit for a Dashboard application.
  2. Always consider performance issues on server-side fetching.
  3. Don’t involve SSR technique at the beginning of the project since your project becomes complicated you could apply it whenever you need to it.
  4. While we have server-side fetching, we need to provide more resource on the server, so need to mature server resource before moving to SSR approach.
  5. You don’t have to follow SSR on entire your application. You could setup SSR just for some particular routes to gain better performance on initial load.

This is my first blog post in English 😅; I hope it would be helpful. I would be thrilled to hear your feedback as well.😄

--

--

Ahmad Tahani

A passionate developer who is hungry to learn new technology and method