Product Design

March 28, 2019

/ code & tools

Next.js features overview

Macin Majewski i Mateusz Anioła

Last time, in the first part of Next.js guide we showed you how this simple framework can simplify rendering React applications. Now we will present some of the basic, useful features which will make your software development in Javascript smoother and easier.

CSS

Next.js by default uses styled-jsx for CSS so all styles between <style jsx></style> tags are isolated for particular page. This way, we don’t have to worry about overwriting them. Styles are being generated in both client and server side so right after entering a website we see a proper layout.  

export default () =>
  <div>
    Hello world
    <p>scoped!</p>
    <style jsx>{`
      p {
        color: blue;
      }
      div {
        background: red;
      }
      @media (max-width: 600px) {
        div {
          background: blue;
        }
      }
    `}</style>
    <style global jsx>{`
      body {
        background: black;
      }
    `}</style>
  </div>

Of course, this solution has its pros and cons but fortunately in Next.js, this element is easy to set up so styled-jsx is not compulsory. You can use CSS-in-JS instead and the basic example would be to use inline styles

<p style={{ color: 'red' }}>hi there</p>

Styled components are also available if you would like to use those! 

It’s also easy to implement CSS preprocessors like Sass, Less or Stylus. To harness the first of them to the project you need to install with yarn@zeit/next-sass and node-sass packages. Next create a _document.js file in the pages catalog and fill it with the following content: 

import Document, { Head, Main, NextScript } from 'next/document'

export default class MyDocument extends Document {
  render() {
    return (
      <html>
        <Head>
          <link rel="stylesheet" href="/_next/static/style.css" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </html>
    )
  }
}

Then, in the next.js setup file (next.config.js) we add code:

const withSass = require('@zeit/next-sass')
module.exports = withSass()

To use scss files inside the components, we just need to import them:

import "../styles.scss"

export default () => <div className="example">Hello World!</div>

Thereby the _document.js file has been created. We will present the details of its performance and purpose later on in the article. For now, it’s needed to add built file containing app styles. Next-sass puts together all styles into one file placed in next/static/style.css, which is presented as /_next/static/style.css by the server so you have to add it manually. 

Metatags

If you have chosen next.js, it means you care about SEO of the app. And everyone who had to deal with search engine optimization knows about the essential role of metatags. Fortunately, there’s no problem with defining own, separate titles, keywords and descriptions for each page. The Head component is made to add them and it’s available directly in Next.js. You need to import Head from 'next/head' and paste the following code in the render component: 

<Head>

      <title key="title">Strona główna</title>

      <meta name="description" content="Strona główna bez prawie żadnej zawartości" key="description" />

      <meta name="keywords" content="strona, główna, brak zawartości" key="keywords" />

</Head>

Everything inside this tag is being transferred to the <head> of the website. There is a key attribute defined on every element that prevents adding the same metatags multiple times and instead overwrites them.  

Lifecycle methods

The basic page files perform like every other component in React which means that they also contain lifecycle methods. Moreover, Next.js added another method - getInitialProps - a static and asynchronous class method. Its goal is to fetch data so a user coming to the website will gain access to all information without the necessity to load additional data by the browser. It also returns the object which is automatically passed as props to a component. GetInitialProps is executed first when you visit the website through a link and also when you change pages within the app itself.  

import React, { Component } from 'react';
import Head from 'next/head';

export default class Home extends Component {
  static async getInitialProps({ req }) {
    const isServer = !!req;

    return { isServer };
  }
  render() {
    return (
      <div>
        {this.props.isServer ? (
          <p>Strona została wyrenderowana przez serwer</p>
        ) : (
          <p>Strona została wyrenderowana od razu w przeglądarce</p>
        )}
      </div>
    );
  }
}

The above example shows you in the browser if the app has been rendered on the server side or the whole process has been run on the browser. You can also use this method with a function component.

getInitialProps receives an object containing:

  • pathname - URL address
  • query - query params from URL turned into an object 
  • asPath - current address in the browser (with query params)
  • req - HTTP request object (server-side only) 
  • res - HTTP response object (server-side only)
  • jsonPageRes - Fetch Response object (browser-side only)
  • err - errors, if they appeared while rendering

Data fetching

As we’ve already mentioned, getInitialProps is used to fetch data from API.  To do that, we need to use async-await feature ES2017, which stops JavaScript code execution till the end of performing the current task. You can also use promises instead of async/await, but that’s the recommended way of handling async operations in getInitialProps.

import React, { Component } from 'react';
import Head from 'next/head';
import 'isomorphic-unfetch';

import MovieBox from '../app/components/MovieBox';

export default class Batman extends Component {
  static async getInitialProps() {
    const response = await fetch('https://api.tvmaze.com/search/shows?q=batman')
    const json = await response.json();

    return { movies: json }
  }

  render() {
    return (
      <div>
        {this.props.movies.map((movie, index) => (
          <MovieBox
            key={index}
            movie={movie}
          />
        ))}
      </div>
    )
  }
}

In the above example, we request Batman movies and receive a JSON object that contains our results. Pass it as props directly to the page render method. The getInitialProps call takes place in both server and client side so you need to use isomorphic-unfetch to smoothly implement fetch (Node.js doesn’t support Fetch API by default!). 

Routing

One of the most basic features of both websites and SPA is navigating from one subpage to another. If you code in React, you need to use React router or other similar solutions. Next.js will do that job for you as it has this system built-in. To define a new router you only need to create a file in pages directory. And that’s it! To redirect to other pages you just need to use ready-made Link component. 

import React from 'react';
import Link from 'next/link';

export default () => (
  <div>
    <p>
      <Link
        href="/o-nas"
      >
        <a>
          Link
        </a>
      </Link>{' '}
      do następnej strony
    </p>
  </div>
);

All you have to do is to add a particular URL address in components href attribute. Thereby, after clicking the link the user will be automatically redirected to the other subpage. You can also add an attribute called “as”, which is used to achieve pretty URLs. It’s a helpful solution when you need to use non-standard routing (we will talk it over later in the article). There’s also a possibility to pass the { pathname: '/about' } object instead of typical href text. 

The link component is not the only way of navigating between pages. In some cases, a software developer has to perform the redirection as a reaction e.g. after sending logging form user should be moved to his profile. Next.js can seamlessly deal with it. To do that, you need to import Router from 'next/router' and call the push or replace function - Router.push('/profil'). You can use this code in any place, e.g. onClick or after resolving a Promise. As well as Link, push and replace functions have a second “as” parameter. There’s also an ability to add URL object instead of an ordinary string.

Individual addresses and servers 

You need to take care of the addresses if you want them to have meaningful and clear names for the users. The default URL is a reflection of the file structure in the pages directory. In some cases, it might be unreadable, especially when you add additional parameters like the article’s slug. Then the address would look like: www.example.com/article?slug=example-article-title. With nonstandard server for request service, you can easily replace this URL with more user-friendly one: www.example.com/article/example-article-title. To achieve that you can use popular Node.js frameworks like express, hapi, koa, etc. After choosing the suitable one you need to create a server.js file and implement code that would handle HTTP requests. 

const express = require('express');
const next = require('next');

const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

app.prepare()
  .then(() => {
    const server = express();

    server.get('/article/:slug', (req, res) => {
      const actualPage = '/article;
      const queryParams = { slug: req.params.name };
      app.render(req, res, actualPage, queryParams);
    });

    server.get('*', (req, res) => {
      return handle(req, res);
    });

    server.listen(3000, '0.0.0.0', (err) => {
      if (err) throw err;
      console.log('> Ready on http://localhost:3000');
    });
  })
  .catch((ex) => {
    console.error(ex.stack);
    process.exit(1);
  });

The example above is made with express. 

In the beginning, you need to import and next, and then do the initial configuration by setting up development or production mode. The crucial thing is in the app.prepare

server.get('/article/:slug', (req, res) => {
  const actualPage = '/article;
  const queryParams = { slug: req.params.name };
  app.render(req, res, actualPage, queryParams);
});

This piece is responsible for reading the address and assigning a “physical” path to a particular page as actualPage. In queryParams we save all the params what we want to pass to the page component. In app.render you have to pass the request, response and newly created variables. For your app to use this new server configuration, you should modify your package.json file so it will look like this: 

"scripts": {
    "dev": "node server.js",
    "build": "next build",
    "start": "NODE_ENV=production node server.js"
},

At first, the default server has been run with next.js, now you need to run your app with the new one. To run it in production mode add properly set environmental variable.

If some default path does suit you, you don’t have to define it. 

server.get('*', (req, res) => {
  return handle(req, res);
});

Thanks to this piece of code, all addresses, which won’t fit defined variables, will be served by a standard next.js routing. 

Modified App component

If the website has some particular layout and you would like to set it automatically on every subpage or you need to get some data from the API before rendering any of the pages, you can modify App component. To do that, you should create ./pages/_app.js file and, according to the documentation, add a simplified App component code:   

import App, {Container} from 'next/app';
import React from 'react';

export default class MyApp extends App {
  static async getInitialProps ({ Component, router, ctx }) {
    let pageProps = {};

    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx);
    }

    return { pageProps };
  }

  render () {
    const { Component, pageProps } = this.props;
    return (
      <Container>
        <Component {...pageProps} />
      </Container>
    )
  }
}

If you analyze the code, you’ll notice that the main goal of App is to pack up the page component in Container, perform getInitialProps and pass this function result as props to the page component. For this reason, getInitialProps is a static method (unlike default class methods which are defined as prototype functions)  - it is executed before initializing the component. 

Overwriting the App component enables to extend the tasks which you need to perform before rendering the page component (e.g. to check if a user has proper permissions). Moreover, you can pack up the component with layout components (e.g. to always show a header above the page component). 

Presented code is only a simplified version of what the App really contains. We recommend checking and analyzing the source code to learn more. Reading through the source code you can draw a lot of valuable conclusions about how Next.js works! 

Modified Document

React apps need the index.html file, which has an elementary HTML structure and contains links to styles, scripts or other static assets. In Next.js apps, you don’t have to create this file because while coding components in pages you never add such HTML or body elements. But if the necessity of extending the basic HTML structure appears, you can do it by adding new ./pages/_document.js file. Creating a new component requires developing Document component. We can find an example of its implementation in the documentation: 

import Document, { Head, Main, NextScript } from 'next/document';

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    return { ...initialProps };
  }

  render() {
    return (
      <html>
        <Head>
          <style>{`body { margin: 0 } /* custom! */`}</style>
        </Head>
        <body className="custom_class">
          <Main />
          <NextScript />
        </body>
      </html>
    );
  }
}

While extending a Document you need to remember about a few restrictions: 

  • A _document.js file is rendered only by the server 
  • Inside a Document we can’t handle events coming from a user (e.g. onClick);
  • Components, which will be placed outside Main won’t be run - if you want to add some permanent elements of the app, like a header, then the proper way to do it is to extend the App component.

Individual error pages 

While browsing a website there might occur a situation when the address we want to visit doesn’t exist. It might be caused by writing a wrong address or because the link hasn’t been updated. Next.js comes to rescue once again! It has built-in error system so when you visit the address, you’ll get a ready-made 404 error site

Error Page Next.jsA very similar site is shown with other errors. 

As you can see this is a simple text on a white background, which may not fit your overall app design. But Next.js has also a very simple mechanism to overwrite this view and this enables us to customize it. To do that you need to create _error.js file in the pages catalog: 

import React from 'react';

export default class Error extends React.Component {
  static getInitialProps({ res, err }) {
    const statusCode = res ? res.statusCode : err ? err.statusCode : null;
    return { statusCode };
  }

  render() {
    return (
      <p>
        {this.props.statusCode
          ? `An error ${this.props.statusCode} occurred on server`
          : 'An error occurred on client'}
      </p>
    )
  }
}

From now on every error generated automatically by Next.js will use newly created error site.

You can even go one step further and show the message in a particular moment e.g. when a user visits a page that doesn’t have a corresponding page file but rather is handled by an article slug request.  We can display the actual page or the error view based on the HTTP status code. You need to import an error component from next/error even when you created a custom error component. When the article fetch request is finished check what’s the response code and render the desired component.

import React from 'react';
import 'isomorphic-unfetch';
import Error from './error';
import Article from '../components/Article';

export default class Article extends React.Component {
  static getInitialProps({ 
    query,
    res,
  }) {
    let statusCode = 200;
    let article = null;
    try {
      article = await fetch(`http://example.com/api/article?slug=${query.slug}`);
    } catch (error) {
      statusCode = error.response.status;
      if (res) {
        res.statusCode = statusCode;
      }

      return { statusCode };
    }

    return { 
      article,
      statusCode,
    };
  }

  render() {
    If (this.props.statusCode !== 200) {
      return (
        <Error statusCode={this.props.statusCode} />
      );
    }

    return (
      <ArticleComponent article={this.props.article} />
    );
  }
}

The article was originally released in polish language on justgeek.it  (part Ipart II). 

See the previous part of the article about Next.js and RSSR integration and stay tuned for the last one dedicated to Next.js setup. 

 

We use cookies on this site to improve performance. By browsing this site you are agreeing to this. For more information see our Privacy policy.