Next.jsとTypeScriptでアプリを作る

# はじめに

Next.js 9がリリースされました。
TypeScriptを標準サポートし、ものすごく簡単にアプリが作れるようになったようなので、試してみましたので、お知らせします。

# やること

1. Next.jsでアプリを作ってみる。
2. WebAPIも作って、アプリから叩けるようにする。

# できたこと

[herokuにデプロイしたもの](https://first-next.herokuapp.com/)
[Github](https://github.com/naoki-tomita/first-next)

# 0. セットアップ

セットアップは簡単でした。
以下のようにライブラリをインストールしましょう。

```sh
$ npm -s i next react react-dom typescript @types/react @types/react-dom @types/node
```

`package.json`のscriptには`next dev`, `next build`, `next start`を書いておきましょう。
Next.jsは基本的にはサーバーサイドレンダリングのフレームワークです。開発時には`next dev`でserve, 本番では`next build`してプロダクションビルドした上で、`next start`することで、余計なコードが除かれた状態でserveできるようになります。

# 1. 適当なアプリを作ってみる

まずは、`next dev`コマンドで開発サーバーを立ち上げておきます。

## 画面を出す

まずは、適当なアプリを作ってみましょう。
基本的にはReactなようなので、Next.js特有のところだけ書いていきます。

ページを用意するには、`pages`フォルダを用意します。
ここに、適当な`tsx`ファイルを用意することで、ルーティングができます。
`pages/foo.tsx`を用意すれば`/foo`で画面にアクセスできるようになります。すごい。
しかも、これ、サーバーサイドレンダリング(SSR)なんですって。簡単。

```tsx:/pages/foo.tsx
import { NextPage } from "next";
const Index: NextPage = props => {
  return (
    <h1>Hello world!</h1>
  );
}
// default exportしないといけない
export default Index;
```

## 画面を出す前に通信をする

シングルページアプリケーション(SPA)ならクライアントで通信するのですが、SSRではサーバー内で取得した上でレンダリングしたHTMLをクライアントにレスポンスします。

レンダリング前にサーバー側で必要なデータを取得するには以下のようにします。

```tsx:/pages/bar.tsx
import { NextPage } from "next";
interface Props {
  data: { foo: string };
}
const Index: NextPage<Props> = props => {
  return <div>{props.data.foo}</div>;
}
                     // async必須。
Index.getInitialProps = async () => {
  // axiosとかで通信する
  // SSRなので、fetchは使えません(node-fetch入れればいける)。
  return { data: { foo: "bar" } };
}
export default Index;
```

`getInitialProps`で得られた値は`props`で得られます。

## 別のページに飛ばしたい

aタグのhrefでいけます。

```tsx
() => {
  return (<a href="/foo/bar">barに飛ぶ</a>);
}
```

## idとか使って動的ルーティングしたい

例えば、`/users`というページがあり、`/users/:id`という動的なパスを持つページがあるとしましょう。
こんなとき、以下のようなジャンプ元の画面と、
(ファイルパスは`/pages/users/index.tsx`と`/pages/users.tsx`のどちらかでよいです。)

```tsx:/pages/users/index.tsx
import Link from "next/link"
const Users = () => {
  // Linkを使って動的ルーティングを実現する。Linkタグの間にはaタグが必要。
  // hrefは、URLのフォーマットを示す。
  // asは実際に飛ばすURLを示す。
  return (
    <ul>
      <li><Link href="/users/[id]" as="/users/0"><a>user1</a></Link></li>
      <li><Link href="/users/[id]" as="/users/1"><a>user2</a></Link></li>
      <li><Link href="/users/[id]" as="/users/2"><a>user3</a></Link></li>
    </ul>
  );
}
```

以下のような、ジャンプ先の画面を用意します。
ファイル名はほんとに`[id].tsx`とします。

```tsx:/pages/users/[id].tsx
import { useRouter } from "next/router";
const User = () => {
  const router = useRouter();
  // ジャンプ元で用意した[id]とquery.idが一致する。
  return <div>userId: {router.query.id}</div>
}
```

あとから気づいたのですが、ジャンプ元の実装はSPAっぽい挙動になるだけでした。
普通に、aタグ使ってhrefで`/users/50`とかやっても普通にルーティングされます。

## 動的ルーティングした結果を元に、SSR前にデータを取りたい

`getInitialProps`と動的ルーティングの複合技です。
ジャンプ元のコードは同じです。

```tsx:/pages/users/[id].tsx
const User = () => {
}
User.getInitialProps = async context => {
  const id = context.query.id; // context.query.idにパスが含まれるので、通信に使うなどしましょう。
}
```

## SPAを作る

普通に、Reactと同じです。
動的ルーティングには`Link`を使いましょう。
`useState`や`useEffect`も使えます。

# 2. WebAPIも作って、SPAから叩けるようにする

APIを作る場合には`/pages/api/`というフォルダの下にファイルまたはフォルダを作ります。
なお、実装は`express.js`の`middleware`に似ている気がしますので、ご存知の方は簡単かもしれません。

```ts:/pages/api/users.ts
import { NextApiRequest, NextApiResponse } from "next";
// export defaultが必要
export default (req: NextApiRequest, res: NextApiResponse) => {
  res.setHeader("content-type", "application/json");
  res.status(200);
  res.end(JSON.stringify({ users: [{ name: "foo" }] }));
}
```

叩く場合は`/api/users`を`GET`するなどしましょう。
なお、ざっと調べた限りでは、GET, POSTなどメソッドによって呼び分けてくれたりするわけではなさそうなので、関数内で自力でメソッド判定するのがよいかと思いました。
※[参考](https://github.com/naoki-tomita/first-next/blob/master/pages/api/piita/items/index.ts)

## WebAPIも動的ルーティングする

こちらはそんなに難しくなく、`req.query`に入っています。
やはりファイル名が重要です。

```ts:/pages/api/users/[id].ts
(req: NextApiRequest) => {
  const id = req.query.id;
}
```

# まとめ
ファイルやフォルダ階層でパスを分けることができるところがわかりやすくてよかったです。
何も考えなくてもSSRできたのも:+1:
今回は無理やりWebAPIも作ってみましたが、基本的にはSSRするので、いらないよなー、と思いました。
覚えるコマンドも三つだし、Herokuにデプロイするならメチャ簡単でした。

以上です。よろしくお願いします。