Kohei Asai

Kohei Asai

Written 08/02/2019
  • English
  • 日本語
  • 7 Tips of Next.js 9 with TypeScript

    The previous time I went over, Next.js was version 7 or 8, I guess. There was a lot of workarounds neccessary and I didn’t feel it worth and gave up to use it because of the cost for workarounds and the outcome from the framework didn't balance. Recently I needed to research how good Next.js 9 is and I made this website using it in order to go over. Here is tips what I run into throughout making this website.

    Next.js is now TypeScript

    Basically Next.js 9 is fully typed. Before this, you needed to install a plugin to use TypeScript and add a few configs in next.config.js and .babelrc. But now you need nothing. Next.js 9 looks totally TypeScript! In fact, there’s Babel is running backside, but you almost never need to care it.

    "esModuleInterop": true in tsconfig.json is supposed to be set for Babel, however, it will be automatically added if it’s not set.

    Write “universal” code in general and “isomorphic” code in getInitialProps()

    All code you wrote will run on both of browsers and Node. At the initial access for each route component, it runs on Node from getInitialProps() as its starting point. After it’s served as a client-side code, it runs as a single page application, which means each route component runs on the browser from getInitialProps(). Therefore, everything in Next.js is supposed to be “universal”.

    Universal: Not dependent to the platform. No reference to APIs only available on browsers or Node such as window nor require("fs") . Only using JavaScript’s standard APIs.

    Isomorphic: Runs on both of browsers and Node but the code is not universal. Even like it refers to window, it checks whether it’s available (e.g. if (typeof window !== "undefined")). Use another way to do something equivalent if it’s not unavailable.

    However, getInitialProps() has arguments req and res when it’s called for server-side rendering. You can distinguish the running platform and fill the difference. This is “isomorphic”. Here is the code to detect user’s locale isomorphicly (I wrote this for the website but I didn’t use for some reason eventually):

    import * as http from 'http';
    import { NextPageContext } from 'next';
    
    SomePageRoute.getInitialProps = ({ req } :NextPageContext) => {
      const locale = decideLocale(req);
    }
    
    // returns the most desired locale
    function decideLocale(req?: http.IncommingMessage): string {
      if (req) {
        // access to HTTP request headers if there's req
        const acceptLanguage = req.headers["accept-language"];
    
        if (acceptLanguage) {
          const requestedLocales = acceptLanguage.split(",").map(part => {
            const [locale, priority] = part.trim().split(";q=");
    
            return { locale, priority: parseInt(priority) };
          });
    
          // sort requested locales by their priority
          requestedLocales.sort((a, b) => b.priority - a.priority);
    
          return requestedLocales.find(({ locale }) => locale !== '*') || DEFAULT_LOCALE;
        }
      }
    
      if (typeof navigator !== 'undefined') {
        // navigator is only available on the browser
        return navigator.languages[0]
      }
    
      return DEFAULT_LOCALE;
    }
    
    const DEFAULT_LOCALE = "en-US"

    Must create _document.tsx and _app.tsx

    There two are supposed to be placed in pages/, however, these are not routes. You can create _document.tsx to change the output HTML structure. Every route components are going to be wrapped by <App> component by creating _app.tsx.

    The default output of Next.js doesn’t have <html>’s lang attribute. You need to set it by yourself (example). In addition, I recommend you to add <meta> elements exactly same throughout every route such as <meta name="viewport" > and <meta name="theme-color" > there (if you want to set <meta> elements individually, you can use Next.js’s <Head> component).

    import * as React from "react";
    import Document, { DocumentContext, Html, Head, Main, NextScript } from "next/document";
    
    interface Props {
      locale: "en-US" | "ja-JP";
    }
    
    class CustomDocument extends Document<Props> {
      render() {
        return (
          <Html lang={this.props.locale.split("-")[0]}>
            <Head>
              <meta name="viewport" content="width=device-width,height=device-height" key="viewport" />
              <link rel="shortcut icon" href="/static/shortcut-icon.png" key="shortcutIcon" />
              <meta name="theme-color" content="#087da1" key="themeColor" />
            </Head>
    
            <body>
              <Main />
    
              <NextScript />
            </body>
          </Html>
        );
      }
    }
    
    export default CustomDocument;

    _app.tsx is useful to mount some common element throughout every page such as navigation bars. Furthermore, it’s also best way to provide certain objects to descendants by React Context (this is well-known pattern to make dependency injection). I supply descendant components “current locale“, “placeholder text for translation” and something like that (example). Here is also the example code for the case that you make a web application and provide “authentication state”.

    import * as React from "react";
    import NextApp, { AppContext, Container } from "next/app";
    import Session from "(somewhere...)";
    import getAuthenticationSession from "(somewhere...)";
    
    interface Props {
      session: Session;
      pageProps: any;
    }
    
    class App extends NextApp<Props> {
      render() {
        const { session, pageProps, Component } = this.props;
    
        return (
          <Container>
            <SessionContext.Provider value={session}>
              <Component {...pageProps} />
            </SelfUrlContext.Provider>
          </Container>
        );
      }
    
      static async getInitialProps({ Component, ctx }: AppContext) {
        const componentGetInitialProps = Component.getInitialProps || (() => Promise.resolve());
    
        const [session, pageProps] = await Promise.all([
          getAuthenticationSession(),
          componentGetInitialProps(ctx),
        ]);
    
        return {
          session,
          pageProps
        };
      }
    }
    
    export default App;

    Don’t rely on useful API Routes so much

    /pages/api/*.tsx are special routes. They don’t have React component to render but can have implementation what HTTP response they return. Which means you can create Web API endpoints with that as like making Web servers .

    Even though you can make “full”-stack web application with both aspect of client-side and server-side, it’s not so great idea. If you make so many Web API endpoints with API routes, it will result in super messy code because you need to carefully write universal code everywhere with Next.js.

    What I recommend is using API routes less. You should use it just to support front-ends. Even like you write Web API endpoints in Node, it’s much better if you build a simple Node API server with Fastify, Koa or Express. Because you don’t need to concern about the bundle size for the client-side code as well as there’s less restriction of the platform.

    Able to serve Sitemap and RSS/Atom feeds

    pages/sitemap.xml.tsx is exposed as /sitemap.xml as the endpoint. In addition, you can control what the endpoint responds for HTTP in getInitialProps() by doing res.send() (example). As React component, you can just return null because no route can be reached in client-side rendering as long as it is not specified by <Link>. This feature enables you to serve Sitemaps or RSS/Atom feeds (As an example, here is Atom feed of this website).

    Consider to use singleton

    pages/_app.tsx is only the module every route passes. You may come up with some idea that it looks a great idea to prepare dependency injection container there and provide it to each route by React's props or contexts. But you need to remember that every page will be in server-side rendering and then going to be application entry point for the single-page application. If you put everything necessary in all routes, Next.js cannot split code into small pieces of application endpoint. This makes huge overhead in the bundle size as well as cause the server-side rendering slow.

    Consider that make modules refers dependencies directly or make some singleton object and make it imports dependencies as less as possible. Even in that case, there's no problem because fortunately most test frameworks have ways to mock modules.

    Does Next.js only run on Now?

    The answer is "No”. ZEIT, which has Now, develops and maintains Next.js as a core team. Surely Now has something like preset for Next.js. Moment after you push your Next.js application to Now, each pages/**/*.tsx will be separately deployed as cloud functions automatically. When you go to some route by a browser, the cloud function runs, do server-side rendering and respond rendered HTML and JS. It works really well.

    My second question was “how about Google Cloud Platform? Amazon Web Services? Netlify?”. The answer is you need to manage it by yourself. Next.js supports making custom single endpoint for server-side in Next.js. It would be the best way for GCP and AWS. As for Netlify, you can generate a normal web application by next export, deploy it to Netlify, and it works as a normal single page application. In this case, you cannot do dynamic server-side rendering with getInitialProps().

    Conclusion

    Next.js 9 is pretty nice. It's the best way to create both of web applications and websites without anything struggling.

    If you can use Now, there is almost no configuration to make web applications working stunningly well. Even if you cannot use Now, you can use Next.js without server-side rendering. Even in this case, this enough worth as considering that the framework includes routing, <head> manipulation, static file serving, CSS in JS and Webpack configuration.