Server-Side Rendering Recipes

Shilpa Patwary
The Startup
Published in
8 min readNov 27, 2019

--

Quick setup guide — SSR with Next.js + react + redux + redux-saga + jest

Are you starting a new project?

Do you want to create a server-side rendered react application?

Are you using redux for state management and redux-saga for side effects?

Do you want to set up a testing framework using Jest?

If you have answered YES to all the above questions, you have landed at the right place. I have attempted to put together all the steps to set up a basic framework in just 15 minutes using Next.js + React + Redux + Redux-Saga + Typescript + Jest.

Before we dive deep into the details of setting up the whole framework, let’s get a quick overview of the different tech-stack we are going to use here. If you don’t need an introduction jump to the setup section.

Next.js

To put it simply, Next.js is a light framework for server-rendered react applications. There are many features that Next.js provides out of the box. Few of them include page based routing, automatic code-splitting, client-side routing, Webpack-based dev environment. The latest version Next.js 9 has shipped features like typeScript support and integrated type-checking and automatic static optimization. To learn more about the features of Next.js you can go through their website. They have a beautiful tutorial that guides you through all the features.

React

Javascript library for building user interfaces.

Redux

A predictable state container for JavaScript apps. There is only one store and the store will dispatch actions. All of the ‘state’ will be located in one place and the store is the single source of truth.

Redux saga

Redux-saga is a redux middleware library, that is designed to make handling side effects in your redux app nice and simple. It achieves this by leveraging an ES6 feature called Generators, allowing us to write asynchronous code that looks synchronous, and is very easy to test.

Jest

Jest is a javascript testing framework that is fast, simple and also provides coverage. It requires little configuration and can be extended to match your requirements.

Now let’s start setting up our framework. By the end, we will have a framework that server renders our react application, a sample page, a sample dynamic route, test setup, sample tests, redux store setup, and sample sagas. You can find the complete template code at https://github.com/shilpapatwary/next-react-redux-saga-starter

Step1: Setting up next

mkdir ssr-next-starter
cd ssr-next-starter
npm init -y
npm install --save react react-dom next
npm install --save-dev typescript @types/node @types/react

Add the following scripts to your package.json

"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}

Now let’s create our page

mkdir pages
cd pages
touch index.tsx

add some sample code to your index file.

const Index = () => (
<div>
<p>SSR with Next.js+ react + redux + redux-saga + jest</p>
</div>
);
export default Index;

Start your development server:

npm run dev

Open http://localhost:3000/ to see your react server-rendered page.

Now let’s add some different pages and dynamic routes to our application. Any file that is created under the pages directory becomes a route in the application. For e.g, if we want an ‘about’ route create about.tsx under /pages.This page can now be accessed at http://localhost:3000/about

about.tsx
const About = () => (
<div> About Page </div>
);
export default About;

Now let’s add some links to our index page to enable routing in our application. Let’s create two types of routes here, a static one and a dynamic one. The dynamic routes are useful when we deal with dynamic data and the pages have to be created dynamically based on some attributes. For e.g, a product detail page is a dynamic route formed with the product id. (localhost:3000/product/1). For dynamic routes create a generic file [id].tsx under /pages/product and add some markup.

We use <Link /> provided by Next Link API, which supports client-side navigation. Let’s add some links to our index.tsx.

// add links in index.tsx
import Link from ‘next/link’;
//static link
<Link href=”about”><a>About</a></Link>
//Dynamic Link
<Link href=”/product/[id]” as={`/product/${1}`}><a>Product 1</a></Link>
//[id].tsx
import { useRouter } from 'next/router';
export default function Product() {
const router = useRouter();
return (
<>
<h1>Product {router.query.id}</h1>
</>
);
}

Now we have a sample react application which is server-rendered using Next.js and a couple of routes. Now we need some additional setup for redux and redux-saga to work in a server-rendered application. Let’s get started!

Step2: Setting up Redux

npm i redux react-redux @types/redux @types/react-redux typesafe-actions// all our components, redux store, types, actions, sagas go here
mkdir src

Now let’s create types, actions and a reducer for our application. Lets put it under /src/redux.

types.ts
import { Action, Store as ReduxStore, Dispatch } from "redux";
export enum SampleReactTypes {
SAMPLE_REACT_TYPE = '@@types/SAMPLE_REACT_TYPE',
SAMPLE_REACT_TYPE_ASYNC = '@@types/SAMPLE_REACT_TYPE_ASYNC'
}
export type Store = ReduxStore<SampleReactState, Action> & {
dispatch: Dispatch;
};
export interface SampleReactState {
data1?: Array<any>
}
actions.ts
import { SampleReactTypes } from './types';
import { action } from 'typesafe-actions';
export const sampleReactAction = () => action(SampleReactTypes.SAMPLE_REACT_TYPE);
export const sampleReactActionAsync = () => action(SampleReactTypes.SAMPLE_REACT_TYPE_ASYNC);reducer.ts
import { SampleReactState, SampleReactTypes } from "./types";
import { AnyAction } from "redux";
const initialState = {
data1: undefined
};
const SampleReducer = (currentState: SampleReactState = initialState, action: AnyAction) => {
switch (action.type) {
case SampleReactTypes.SAMPLE_REACT_TYPE_ASYNC:
return getSampleContentReducer(currentState, action);
default:
return currentState;
}
};
function getSampleContentReducer(currentState: SampleReactState, action: AnyAction) {
const data = action.payload || action.data;
return Object.assign({}, currentState, { data1: data });
}
export default SampleReducer;

Using Redux with SSR

When we are using redux with server-side rendering we need to handle it a bit differently, as the server needs to send the initial state with the response to the client-side. In cases where the data is prefetched before generating the markup, if the client-side doesn’t have access to the data, we might land into issues of different markups on server and client and the client might have to load the data again.

This is how we need to handle redux on both server and client

redux on the server: provide initial state of the app

  1. Create a new redux store instance on every request
  2. dispatch actions if any
  3. pull the state out of store, pass the state to the client

redux on the client:

  1. A new redux store is created and initialized with the state provided by the server.

next-redux-wrapper is a node module that can be used for setting up redux for server-side rendering. Let’s use this module to speed up things.

npm i next-redux-wrapper

Now lets park this here and set up our sagas and then wire everything together.

Step3: Setting up Redux Sagas

npm i redux-saga isomorphic-unfetchsrc/sagas/sagas.ts
import { takeEvery, put, call, fork } from 'redux-saga/effects';
import { SampleReactTypes } from '../redux/types';
import { getSampleContent } from './apis';
export function* sampleContentAsync() {
const data = yield call(getSampleContent);
yield put({type: SampleReactTypes.SAMPLE_REACT_TYPE_ASYNC, data})
}
function* watchSampleContent() {
yield takeEvery(SampleReactTypes.SAMPLE_REACT_TYPE, sampleContentAsync)
}
export default function* root() {
yield fork(watchSampleContent);
}
src/sagas/apis.ts
import fetch from 'isomorphic-unfetch';
export async function getSampleContent(){
const response = await fetch('https://api.myjson.com/bins/cdxq0');
const body = await response.json();
if (response.status !== 200) throw Error(body.message);
return body;
}

We can fetch data using an async function called getInitialProps. This function can be used to fetch data for a page via a remote data source and pass it as props to our page.

static async getInitialProps({ isServer, store }) {
store.dispatch({ type: SampleReactTypes.SAMPLE_REACT_TYPE});
return {};
}

But as we are using redux if we try to dispatch any async actions to get the data from our index page just like we do on our client rendered react applications, we might run into some issues.

Over the next sections, let’s see how we can dispatch actions and get dynamic data for our application, the challenges we face while using async calls to get data in our server-rendered application and how we can fix it.

Using Redux sagas with SSR

Issues to handle: If we dispatch our action through getInitialProps and try to load our page, we see that the client renders before the async action completes.

We need to make sure we await the saga task before sending the results to the client. The node module next-redux-saga provides the ability to achieve this.

npm i next-redux-saga

Step4: Wiring everything — creating store.ts

Lets set up our store and add a new file /pages/_app.js, where we use two higher-order components to create and provide the redux store to all our pages.

/store/store.tsimport { createStore, applyMiddleware } from "redux";
import JobsApplicationReducer from "../redux/reducer";
import createSagaMiddleware, { END } from "redux-saga";
import root from "../sagas/sagas";
import { SampleReactState } from "../redux/types";
const makeStore: any = (initialState: SampleReactState) => {
const sagaMiddleware = createSagaMiddleware();
const store: any = createStore(
JobsApplicationReducer,
initialState,
applyMiddleware(sagaMiddleware)
);
store.sagaTask = sagaMiddleware.run(root);
return store;
};
export default makeStore;
/pages/_app.tsx
import React from "react";
import {Provider} from "react-redux";
import App from "next/app";
import withRedux from "next-redux-wrapper";
import withReduxSaga from 'next-redux-saga';
import makeStore from '../src/store/store';
class MyApp extends App<any, any> {
static async getInitialProps({Component, ctx}) {
const pageProps = Component.getInitialProps ? await
Component.getInitialProps(ctx) : {};
return { pageProps };
}
render() {
const {Component, pageProps, store} = this.props;
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>);
}
}
export default withRedux(makeStore)(withReduxSaga(MyApp));

Let’s change our index page to dispatch some actions to our redux store.

import React, { Component } from "react";
import Link from "next/link";
import { SampleReactTypes } from '../src/redux/types';
import { connect } from "react-redux";
interface Props {
data1: Array<any>;
}
class Index extends Component<Props, any> {
static async getInitialProps({isServer, store}) {
await store.dispatch({type: SampleReactTypes.SAMPLE_REACT_TYPE});
return {}
}
render() {
return <>
<header>
<Link href="about"><a>About</a></Link>
</header>
<div>
<p>SSR with Next.js+ react + redux + redux-saga + typescript
+ jest</p>
</div>
{this.props.data1 && <span>Data loaded:
{this.props.data1.length} items</span> }
<ul>
<li><Link href="/product/[id]" as={`/product/${1}`}>
<a>Product 1</a></Link>
</li>
</ul>
</>
}
}
export default connect(state => state)(Index);

Now if we run our application using npm run dev we can see the dynamic content length displayed on the screen — Data loaded: 9 items.

Now we have our sample application that uses redux and sagas and is server-rendered.

One more step to go.Let’s set up our tests!

5. Testing

Let’s start testing! We will start by installing a few modules that will be required for testing.

npm i -- save-dev chai enzyme enzyme-adapter-react-16 jest @types/enzyme @types/enzyme-adapter-react-16 @types/jest babel-jest @types/chai

Let’s set up our jest framework. In the root directory of the project create jest.setup.js , .babelrc and jest.config.js

jest.setup.js
const Enzyme = require(‘enzyme’)
const Adapter = require(‘enzyme-adapter-react-16’)
Enzyme.configure({ adapter: new Adapter() })

Let’s define the Jest configuration.

jest.config.js
const TEST_REGEX = ‘(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|js?|tsx?|ts?)$’
module.exports = {
setupFiles: ['<rootDir>/jest.setup.js'],
testRegex: TEST_REGEX,
transform: {
'^.+\\.tsx?$’: 'babel-jest'
},
testPathIgnorePatterns: [‘<rootDir>/.next/’,
‘<rootDir>/node_modules/’],
moduleFileExtensions: [‘ts’, ‘tsx’, ‘js’, ‘jsx’],
collectCoverage: false,
moduleNameMapper: {
“.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$”: “identity-obj-proxy”
}
}
.babelrc
{
"presets": ["next/babel"]
}

Let’s add a test script to our package.json

“test” : “jest”

Now let’s create a sample component and a simple test.

/src/components/SampleComponent.tsx
import React from 'react';
class SampleComponent extends React.Component<any, any> {
render(){
return <div className="content">Sample Component</div>
}
}
export default SampleComponent;
/src/components/SampleComponent.spec.tsx
import Enzyme, { shallow, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import * as React from 'react';
import { should } from 'chai';
import SampleComponent from './SampleComponent';
should();
Enzyme.configure({ adapter: new Adapter() });
describe("<SampleComponent />", () => {
it('should dispatch sample action', () => {
const wrapper = mount(<SampleComponent></SampleComponent>);
wrapper.find(".content").length.should.equal(1);
});
});

Run the test npm test , and you should see our test passing.

Now, we have our starter ready! Get started with building your application.

--

--