React: Next

インストール

TypeScript と Sass も使えるようにする。

yarn add next react react-dom @zeit/next-typescript @zeit/next-sass node-sass
yarn add -D typescript @types/next @types/react @types/react-dom

Redux とか rxjs を使うときはこれもいれる。

yarn add next-redux-wrapper react-redux redux redux-observable redux-logger rxjs
yarn add -D @types/next-redux-wrapper @types/react @types/react-redux @types/redux @types/redux-logger babel-plugin-module-resolver

next.config.jsを作る。

const withTypescript = require("@zeit/next-typescript");
module.exports = withTypescript();

VSCode の設定

ext install esbenp.prettier-vscode

ルール

pagesディレクトリの中にあるコンポーネントは自動で URL と紐づけられる

pages
  - Index.jsx
  - About.jsx
  - contents
    - ContentA.jsx

この場合、以下のページが存在することになる。

  • localhost:3000/index
  • localhost:3000/about
  • localhost:3000/contents/contentA

共通コンポーネントや util 的なコンポーネントはpages以外に置く。

基本

共通レイアウト

ヘッダ、サイドバーなどの共通レイアウト用にコンポーネントを作成できる。

components/Layout.jsx

import Header from "./Header";

const Layout = props => (
  <div>
    <Header />
    {props.children}
  </div>
);

export default Layout;

pages/index.jsx

import Layout from "../components/MyLayout.js";

export default () => (
  <Layout>
    <p>Hello Next.js</p>
  </Layout>
);

このようにpages配下の各コンポーネントから利用できる。

ページ遷移

Linkコンポーネントを使う。

import Link from "next/link";

<Link href="/about">
  <a>About Page</a>
</Link>;

これはpages/about.jsxへ遷移するリンク。

イベントハンドラなどで遷移したい場合はRouter.push()を使う。

import Router from "next/router";

const Login = () => {
  const handleClick = (e: any) => Router.push("/");

  return <p onClick={handleClick}>Login...</p>;
};

export default Login;

動的ページ生成

hrefにクエリパラメータを書いて遷移もできる。

<Link href="/post?title=hoge">
  <a>Go to post</a>
</Link>

pages/post.jsx

import { withRouter } from "next/router";

const Post = withRouter(props => <h1>{props.router.query.title}</h1>);

export default Post;

withRouter()を使ってコンポーネントを作るとprops.router.queryでクエリパラメータを参照できるようになる。

クリーン URL

Linkコンポーネントのasプロパティに URL の別名を指定できる。

const Link = props => (
  <Link as={`/p/${props.id}`} href={`/post?title=${props.title}`}>
    <a>{props.title}</a>
  </Link>
);

pages/post.jsxに遷移するが、URL は/p/${props.id}となる。

サーバーサイドレンダリング

クリーン URL を使うとF5更新で 404 になってしまう。
カスタムサーバーを使うとこの問題を回避できる。
expressを利用する。

yarn add express

server.js

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();

    // ここでカスタムルートを定義する
    // この場合、`post.jsx`ではクエリパラメータの`title`(=id)をもとにサーバーからデータを取得する形になる
    server.get("/p/:id", (req, res) => {
      const actualPage = "/post";
      const queryParams = { title: req.params.id };
      app.render(req, res, actualPage, queryParams);
    });

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

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

package.jsonを書き換える。

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

yarn devし、クリーン URL へ遷移してからF5更新が正常に行われることがわかる。

サーバーからデータを取得

データ取得ライブラリにはisomorphic-unfetchを使う。
クライアント環境もサーバーサイド環境も対応しているらしい。

yarn add isomorphic-unfetch

初期データ取得用にgetInitialPropsが使える。

import Link from "next/link";
import fetch from "isomorphic-unfetch";

const Index = props => (
  <div>
    <ul>
      {props.shows.map(({ show }) => (
        <li key={show.id}>
          <Link as={`/p/${show.id}`} href={`/post?id=${show.id}`}>
            <a>{show.name}</a>
          </Link>
        </li>
      ))}
    </ul>
  </div>
);

// 引数のpropsからクエリパラメータを受け取れる
// ex) props.query.title
Index.getInitialProps = async function(props) {
  const res = await fetch("https://api.tvmaze.com/search/shows?q=batman");
  const data = await res.json();

  console.log(`Show data fetched. Count: ${data.length}`);

  return {
    shows: data
  };
};

export default Index;

クエリパラメータをgetInitialProps()の中で参照する

contextを引数にとると、context.queryで参照できる。

Index.getInitialProps = async function(context) {
  const { id } = context.query;
  const res = await fetch(`https://api.tvmaze.com/shows/${id}`);
  const show = await res.json();

  console.log(`Fetched show: ${show.name}`);

  return { show };
};

Typescript と使う

@zeit/next-typescript

FAQ

reactWEBPACK_IMPORTED_MODULE_0.useEffect is not a function

webpack で React.use***()を使ったときに、React のバージョンが正しくない場合におこる。
https://stackoverflow.com/questions/53024307/typeerror-dispatcher-usestate-is-not-a-function-when-using-react-hooks

package.jsonを以下のように書き換えて解決。

"devDependencies": {
  "react": "16.7.0-alpha.2",
  "react-dom": "16.7.0-alpha.2",
}

Hooks can only be called inside the body of a function component

React のコンポーネントライブラリを読み込んだときなどに、React のインスタンスを複数生成してしまった場合に起こる。
バージョン違いの React を使用していると起こるらしい。

import * as React from "@hako1912/my-lib";
// import * as React from 'react' の代わりに

react-hot-loaderを入れる

yarn add react-hot-loader

スニペット

ローディング中は別のコンポーネントを表示する

const component = () => {
  const [loading, setLoading] = useState(true);

  React.useEffect(() => {
    Router.push("/login");
  });

  return <>{loading ? <p>loading...</p> : <Main />}</>;
};

環境変数を使う

dotenv-webpackを使う。

yarn add dotenv-webpack

next.config.jsを修正する。

// 必ず先頭で実行
require("dotenv").config();

const Dotenv = require("dotenv-webpack");
const path = require("path");

module.exports = webpack(config) {
        config.plugins = config.plugins || [];
        config.plugins = [
          ...config.plugins,
          // Read the .env file
          new Dotenv({
            path: path.join(__dirname, ".env"),
            systemvars: true
          })
        ];
        return config;
      }
    })
  )
);

.envファイルをプロジェクトルートに作成する。

TEST=hogehoge

上記の設定をすると、process.env.<環境変数名>で環境変数を参照できるようになる。
process.env の値がwebpack build時に環境変数で置き換わる。

console.log(process.env.TEST);

TypeScript の補完をprocess.envに対して適用したいときは、以下のようにProcessEnvの定義を追加する。

/// <reference types="node" />

declare namespace NodeJS {
  interface ProcessEnv {
    readonly HOGE: string;
    readonly FUGA: string;
  }
}

ただし、d.tsで既存の interface の型を減らすことはできないので、どんな値を参照してもエラーにはならない。

// ProcessEnvのもともとの定義
interface ProcessEnv {
  [key: string]: string | undefined;
}

Ref:
https://github.com/zeit/next.js/tree/master/examples/with-dotenv