Web Development

Build Isomorphic Apps with Next.js

May 15th, 2019 | By Camilo Reyes | 4 min read

React opens ways to render components anywhere through its virtual DOM. As a result, rendering is not tightly coupled to a browser.

Next.js unlocks server-side rendering by default — using React components that render on both client and server.

In this take, we’ll be building a simple timer component. It keeps track of elapsed minutes and seconds and updates once every second.

The app is isomorphic, rendering on the client and server. The same logic gets reused to load the component and fire updates. Universal rendering is done by Next.js.

For example, to fire updates every second:

componentDidMount() {
  this.intervalTimer = setInterval(() => this.increase(), 1000);
}


For Next.js, you can use whatever tools already work with React. For this demo, I’ll pick TypeScript, Redux, and Enzyme.

There is plumbing required to get Redux and TypeScript to work with Next.js. The App component, for example, needs a custom extension so the store can go in a <Provider />. This is the Redux store necessary when it loads the component. App props need both the default set with Next.js and the custom store. This same store is used for universal rendering in Next.js.

To set up TypeScript, Next.js has a plugin @zeit/next-typescript one can configure. We’ll forgo all the plumbing to keep the sample code focused. Feel free to check out the GitHub repo for more details.

When the component is ready in Next.js, you render it through an index route. For example:

const IndexPage: React.FC = () => <Timer />;


Next.js has a pages folder where each .tsx or .ts file becomes a route. The rest gets rendered by the framework and React. The index page doesn’t concern itself with prop parameters. These parameters are encapsulated in a state machine like Redux.

TypeScript

The timer needs a state object to keep track of the minutes and seconds. Dispatched actions will increase the timer in seconds. The component props encapsulate both the current state and the action function.

For TypeScript, we can build these concepts using type annotations. This allows the app to scale as it grows with more requirements. Having a set of static types makes it easier to refactor and make changes at will.

For example:

interface TimerState {
  seconds: number;
  minutes: number;
}

const INCREASE_SECONDS = 'INCREASE_SECONDS';

interface IncreaseTimerAction {
  type: typeof INCREASE_SECONDS;
}

type TimerActionTypes = IncreaseTimerAction;

interface TimerProps {
  timer: TimerState;
  increaseTimer: () => void;
}

interface ReduxAppProps extends AppProps {
  reduxStore: Store;
}


Note that ReduxAppProps extends the default Next.js AppProps type. This is how we define a custom Redux store. For dispatched actions, we encapsulate all actions through a single type. If there are more actions, add it with a union type.

In TypeScript, you do this through a pipe, for example, Action1Type | Action2Type. This keeps the reducer from having to worry about too many action types.

Redux

or Redux, the most significant piece is the reducer. This is where dispatched actions go to get to the next state.

The reducer gets the initial state and the dispatched action. The initial state comes from a default parameter when it first loads. We’ll use the types declared in TypeScript to nail down type contracts. This adds a way of communicating intent in the reducer to each kind.

Many action types can go into the reducer, which makes the code scalable. For example:

const reducer = (state = initialTimerState,
  action: TimerActionTypes): TimerState => {
  switch (action.type) {
    case INCREASE_SECONDS:
      const isOverAMinute: boolean = state.seconds >= 59;

      return {
        seconds: isOverAMinute
          ? 0 : state.seconds + 1,
        minutes: isOverAMinute
          ? state.minutes + 1 : state.minutes
      };

    default:
      return state;
  }
};


This handles the logic of rolling the seconds over when it goes over a minute. Note that, with each call, we return a new state object.

Next.js can execute this code both on the client and the server. There is no special code necessary to get this to work. Also note the use of types using a colon, for example: TimerState.

This tells TypeScript to do type-checking during compilation. You typically do an npm run type-check to run the compiler. The type checker can run during a build and block any commits that break any contracts.

Enzyme

The timer component shows the minutes and seconds separated by a colon. It pads both with a zero when they are below ten. For example, 00:00. There is a componentDidMount method that starts the timer with a dispatched action.

The enzyme can shallow render the component so Jest can verify how the timer renders. For example:

it('pads minutes and seconds', () => {
  const component = shallow(<TimerComponent
    timer={{seconds: 0, minutes: 0}}
    increaseTimer={() => {}} />);

  expect(component.find('p').text()).toEqual('00:00');
  component.unmount();
});


To clear out the interval set by setInterval, be sure to unmount the component. This is called the componentWillUnmount method. Note the use of find to query the virtual DOM for a p tag. Think of Enzyme as the jQuery for testing React components.

Changing the seconds parameter into a string trips the type checker and throws a compiler error. This turns this unit test into sound code that does what the type contracts say it should do.

Jest works with TypeScript out of the box as of the latest version. Be sure to set the Enzyme adapter and check that it matches the React version, for example, enzyme-adapter-react-16. Jest needs to know about this adapter configuration through jest.config.js. Note the component is isolated enough to where it doesn’t depend on Redux or Next.js.

Conclusion

Working with Next.js is a lot like working with React. If you are familiar with React, then Next.js feels like home.

Next.js offers code splitting, filesystem-based routing, and hot code reloading out of the box. These are some very advanced features that work well with React components.

TypeScript plays well with the rest of the tools and does not get in the way.

The app is isomorphic because it does universal rendering. This is super nice to have since it reduces load times in the browser. Meaning, it doesn’t have to wait on JavaScript for the initial page to load.

Jscrambler

The leader in client-side Web security. With Jscrambler, JavaScript applications become self-defensive and capable of detecting and blocking client-side attacks like Magecart.

View All Articles

Must read next

Javascript

Developing An Isomorphic GraphQL App With React

Check more info about isomorphic programming in JavaScript with this short demo, while leveraging React and GraphQL

July 21, 2016 | By João Carvalho | 4 min read

Web Security

Monkey Patching: An Analysis of Code Poisoning JavaScript

Monkey patching enables dynamically changing the behavior of JavaScript. While it can be used legitimately, it can also enable attacks like Magecart.

October 15, 2019 | By Jscrambler | 6 min read

Section Divider