Next.jsをReasonMLで用いてページ遷移しWeb APIを叩きレスポンスを表示する

# Next.jsとは
[Next.js](https://nextjs.org/)
Next.jsはReact.jsを用いたフロントエンドJavascriptフレームワークです。
SSRが容易に実現できSEO対策として優れる点やファイル構成がそのままページ構成になるので、ルーティングが楽な点が好きです。

# ReasonMLとは
[Reason](https://reasonml.github.io/ja/)
OCamlベースの言語です。BuckleScriptによってJSにトランスパイルされます。
静的型付けでイミュータル、そして関数型言語らしくパターンマッチングが使えます。
Facebookの技術なので、もちろん(?)JSXにも対応してます。
FacebookはOCamlが好きで、JSの静的型チェッカーFlowも大部分がOcamlで書かれています。

# ReasonMLを用いる動機
Web開発をするにあたってJS/AltJSを考えると、ECMAScript, TypeScript, ElmそしてBuckleScript/ReasonMLと選択肢があり、Reactを使うことを考えるとElmは使えず、ECMAScript+Flow, TypeScript, BuckleScript/ReasonMLのどれかになります。
ECMAScript+FlowもTypeScriptも使う人が多く、特に困ることはないと思います。
私自身、最近Haskellを勉強していて、関数型を学んでいくとオブジェクトがイミュータブルだったりパターンマッチングが使えるReasonMLが良いのではと思った次第です。

# 初期セットアップ
[Example app using ReasonML & ReasonReact components](https://github.com/vercel/next.js/tree/master/examples/with-reasonml)
実はNext.jsのリポジトリの中に説明が用意されています。
これに従ってコマンドを実行します。

```
$ yarn create next-app --example with-reasonml <任意の新規作成アプリ名>
```

フォルダ構成は以下の通り

```
.
├── README.md
├── bindings
│   └── Next.re
├── bsconfig.json
├── components
│   ├── Counter.re
│   ├── GlobalCount.re
│   └── Header.re
├── index.js
├── next.config.js
├── node_modules(略)
├── package.json
├── pages
│   ├── about.re
│   └── index.re
├── tree.txt
└── yarn.lock
```

binding/Next.reにNext.jsとをReasonMLから利用するためのモジュール設定が書かれています。
しかし、これらが全てではないので、足りない場合はJSのコードを参照して補わないといけません。

package.jsonないのscriptsにコマンドが書かれています。

```
$ yarn dev
```

で開発モードでサーバーが起動します。
http://localhost:3000/
でサンプルページが開けるので確認しましょう。

![ss_index.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68992/58821ca2-c39f-078a-3bf2-b9d246358d66.png)

#sampleページを作る

`$ yarn dev`が走っている状態で新しくページを追加してみましょう。
pages内にsample.reを作成します。

```ocaml:sample.re
[@react.component]
let make = () => {
  <div> <p> {ReasonReact.string("test")} </p> </div>;
};

let default = make;
```

保存すると、再度トランスパイルが走り、
http://localhost:3000/sample
でページが表示されます。

![ss_sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68992/9e0537c7-e684-5f9b-0f94-1af144692d04.png)

この手軽さがNext.jsの良いところです。

make関数はreact.componentを返り値に持つ関数で、これをdefaultに渡しています。
ReasonMLの関数では最後の値が返り値になります。パターンマッチングを用いることが多く、アーリーリターンは使わないようです。
JavaScript版(TypeScript含む。以後JS版)で関数コンポーネントと称されていたものと似てますね。
[関数コンポーネントとクラスコンポーネント](https://ja.reactjs.org/docs/components-and-props.html#function-and-class-components)

トランスパイルされたものを見てみましょう

```javascript:sample.bs.js
// Generated by BUCKLESCRIPT, PLEASE EDIT WITH CARE

import * as React from "react";

function Sample(Props) {
  return React.createElement("div", undefined, React.createElement("p", undefined, "テスト"));
}

var make = Sample;

var $$default = Sample;

export {
  make ,
  $$default ,
  $$default as default,
  
}
/* react Not a pure module */
```

ReasonMLで面倒なのが、`{ReasonReact.string("test")}`の部分です。
文字列を出力するにあたっていちいち囲まなくてはいけません。
また、`"test"`を`"テスト"`に置き換えた時、文字化けします。

![ss_sample_utf8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68992/e53ddc96-d477-d1f3-69c9-0ee4a229cbc5.png)

これを回避するために、エスケープしてあげます。
ダブルクオーテーション(`""`)の代わりに、`{js||js}`あるいは`{j||j}`でくくります。
[String & Character - Reason](https://reasonml.github.io/docs/ja/string-and-char)

```ocaml:pages/sample.re
[@react.component]
let make = () => {
  <div> <p> {ReasonReact.string({js|test|js})} </p> </div>;
};

let default = make;
```

![ss_sample_utf8_escape.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68992/29b2b9be-82e0-2109-eea0-8490bd9a9dcb.png)

# NextRouterで遷移
Next.jsで画面を遷移させる方法として、JSXにLinkを埋め込む方法と関数内で遷移させる方法が考えられます。
[Next.js - Routing](https://nextjs.org/docs/routing/introduction)

## Next.Link
アプリを作成したときに生成されるcomponentsフォルダのHeader.reを見てください。

```ocaml:components/Header.re
let styles = ReactDOMRe.Style.make(~marginRight="10px", ());

[@react.component]
let make = () => {
  <div>
    <Next.Link href="/">
      <a style=styles> {ReasonReact.string("Home")} </a>
    </Next.Link>
    <Next.Link href="/about">
      <a style=styles> {ReasonReact.string("About")} </a>
    </Next.Link>
  </div>;
};

let default = make;
```
`<Next.Link href="/">(中略)</Next.Link>`や`<Next.Link href="/about">(中略)</Next.Link>`で指定される部分がリンクです。

![ss_index_link.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68992/5538e487-f868-de86-cb90-285b73fdba0b.png)

文字列をクリックすると遷移できます。

## 関数内で制御(Router.push)

### モジュール接続

関数内で遷移させる場合、useRouterを用いてrouterオブジェクトにアクセスします。
[Next.js - useRouter](https://nextjs.org/docs/api-reference/next/router#userouter)

このページにある例を見てみます。

```javascript:active_link.js
import { useRouter } from 'next/router'

function ActiveLink({ children, href }) {
  const router = useRouter()
  const style = {
    marginRight: 10,
    color: router.pathname === href ? 'red' : 'black',
  }

  const handleClick = (e) => {
    e.preventDefault()
    router.push(href)
  }

  return (
    <a href={href} onClick={handleClick} style={style}>
      {children}
    </a>
  )
}

export default ActiveLink
```
ファイル冒頭で`import { useRouter } from 'next/router'`のよりuseRouterをインポートし、
function内で`const router=useRouter()`で呼び出し、
handleClick内で`router.push(href)`で遷移させるようになっています。

では、これをReasonMLで実現するためにはどうすればよいでしょうか。

初期セットアップの段でNext.jsのモジュール設定はNext.reにあると説明しました。ということでまず、`bindings/Next.re`を参照し、中身を確認します。

```ocaml:bindings/Next.re
module Link = {
  [@bs.module "next/link"] [@react.component]
  external make:
    (
      ~href: string=?,
      ~_as: string=?,
      ~prefetch: option(bool)=?,
      ~replace: option(bool)=?,
      ~shallow: option(bool)=?,
      ~passHref: option(bool)=?,
      ~children: React.element
    ) =>
    React.element =
    "default";
};

module Head = {
  [@bs.module "next/head"] [@react.component]
  external make: (~children: React.element) => React.element = "default";
};

module Error = {
  [@bs.module "next/head"] [@react.component]
  external make: (~statusCode: int, ~children: React.element) => React.element =
    "default";
};
```

どうもサンプルプロジェクト用の最低限のモジュール分しか実装されていないようです。
では、useRouterを実装するべく、モジュール本体を確認しましょう。
まず、`node_modules/next/router.d.ts`をチェックします。

```typescript:node_modules/next/router.d.ts
export * from './dist/client/router'
export { default } from './dist/client/router'
```

すると、別の場所からインポートしてエクスポートしているようなので、該当の
`node_modules/next/dist/client/routeｒ.d.ts`を見ます。

```typescript:node_modules/next/dist/client/router.d.ts
/// <reference types="node" />
import React from 'react';
import Router, { NextRouter } from '../next-server/lib/router/router';
declare type SingletonRouterBase = {
    router: Router | null;
    readyCallbacks: Array<() => any>;
    ready(cb: () => any): void;
};
export { Router, NextRouter };
export declare type SingletonRouter = SingletonRouterBase & NextRouter;
declare const _default: SingletonRouter;
export default _default;
export { default as withRouter } from './with-router';
export declare function useRouter(): NextRouter;
export declare const createRouter: (pathname: string, query: import("querystring").ParsedUrlQuery, as: string, __3: {
    subscription: (data: {
        Component: React.ComponentType<{}>;
        __N_SSG?: boolean | undefined;
        __N_SSP?: boolean | undefined;
        props?: any;
        err?: Error | undefined;
        error?: any;
    }, App?: React.ComponentClass<{}, any> | React.FunctionComponent<{}> | undefined) => Promise<void>;
    initialProps: any;
    pageLoader: any;
    Component: React.ComponentType<{}>;
    App: React.ComponentType<{}>;
    wrapApp: (App: React.ComponentType<{}>) => any;
    err?: Error | undefined;
    isFallback: boolean;
}) => Router;
export declare function makePublicRouterInstance(router: Router): NextRouter;
```

userRouterが返すNextRouter型は`node_modules/next-server/lib/router/router.d.ts`に記載があるようです。

```typescript:node_modules/next-server/lib/router/router
/// <reference types="node" />
import { ParsedUrlQuery } from 'querystring';
import { ComponentType } from 'react';
import { UrlObject } from 'url';
import { MittEmitter } from '../mitt';
import { NextPageContext } from '../utils';
export declare function addBasePath(path: string): string;
export declare function delBasePath(path: string): string;
declare type Url = UrlObject | string;
declare type ComponentRes = {
    page: ComponentType;
    mod: any;
};
export declare type BaseRouter = {
    route: string;
    pathname: string;
    query: ParsedUrlQuery;
    asPath: string;
    basePath: string;
};
export declare type NextRouter = BaseRouter & Pick<Router, 'push' | 'replace' | 'reload' | 'back' | 'prefetch' | 'beforePopState' | 'events' | 'isFallback'>;
export declare type PrefetchOptions = {
    priority?: boolean;
};
declare type RouteInfo = {
    Component: ComponentType;
    __N_SSG?: boolean;
    __N_SSP?: boolean;
    props?: any;
    err?: Error;
    error?: any;
};
declare type Subscription = (data: RouteInfo, App?: ComponentType) => Promise<void>;
declare type BeforePopStateCallback = (state: any) => boolean;
declare type ComponentLoadCancel = (() => void) | null;
declare type HistoryMethod = 'replaceState' | 'pushState';
export default class Router implements BaseRouter {
    route: string;
    pathname: string;
    query: ParsedUrlQuery;
    asPath: string;
    basePath: string;
    /**
     * Map of all components loaded in `Router`
     */
    components: {
        [pathname: string]: RouteInfo;
    };
    sdc: {
        [asPath: string]: object;
    };
    sub: Subscription;
    clc: ComponentLoadCancel;
    pageLoader: any;
    _bps: BeforePopStateCallback | undefined;
    events: MittEmitter;
    _wrapApp: (App: ComponentType) => any;
    isSsr: boolean;
    isFallback: boolean;
    static events: MittEmitter;
    constructor(pathname: string, query: ParsedUrlQuery, as: string, { initialProps, pageLoader, App, wrapApp, Component, err, subscription, isFallback, }: {
        subscription: Subscription;
        initialProps: any;
        pageLoader: any;
        Component: ComponentType;
        App: ComponentType;
        wrapApp: (App: ComponentType) => any;
        err?: Error;
        isFallback: boolean;
    });
    static _rewriteUrlForNextExport(url: string): string;
    onPopState: (e: PopStateEvent) => void;
    update(route: string, mod: any): void;
    reload(): void;
    /**
     * Go back in history
     */
    back(): void;
    /**
     * Performs a `pushState` with arguments
     * @param url of the route
     * @param as masks `url` for the browser
     * @param options object you can define `shallow` and other options
     */
    push(url: Url, as?: Url, options?: {}): Promise<boolean>;
    /**
     * Performs a `replaceState` with arguments
     * @param url of the route
     * @param as masks `url` for the browser
     * @param options object you can define `shallow` and other options
     */
    replace(url: Url, as?: Url, options?: {}): Promise<boolean>;
    change(method: HistoryMethod, _url: Url, _as: Url, options: any): Promise<boolean>;
    changeState(method: HistoryMethod, url: string, as: string, options?: {}): void;
    getRouteInfo(route: string, pathname: string, query: any, as: string, shallow?: boolean): Promise<RouteInfo>;
    set(route: string, pathname: string, query: any, as: string, data: RouteInfo): Promise<void>;
    /**
     * Callback to execute before replacing router state
     * @param cb callback to be executed
     */
    beforePopState(cb: BeforePopStateCallback): void;
    onlyAHashChange(as: string): boolean;
    scrollToHash(as: string): void;
    urlIsNew(asPath: string): boolean;
    /**
     * Prefetch page code, you may wait for the data during page rendering.
     * This feature only works in production!
     * @param url the href of prefetched page
     * @param asPath the as path of the prefetched page
     */
    prefetch(url: string, asPath?: string, options?: PrefetchOptions): Promise<void>;
    fetchComponent(route: string): Promise<ComponentRes>;
    _getData<T>(fn: () => Promise<T>): Promise<T>;
    _getStaticData: (asPath: string) => Promise<object>;
    _getServerData: (asPath: string) => Promise<object>;
    getInitialProps(Component: ComponentType, ctx: NextPageContext): Promise<any>;
    abortComponentLoad(as: string): void;
    notify(data: RouteInfo): Promise<void>;
}
export {};
```

これでuseRouterを用いるのに必要な型情報がわかりましたね！！
じゃあこれに基づいてNext.reに実装していきましょう、と言いたいのですが、どうすればいいのかわからない、というのが正直なところ。

こういう時、大抵探せば見つかるものです。既に実装している人がいるので確認します。
[mrmurphy/reason-nextjs - reason-nextjs/src/Next.re](https://github.com/mrmurphy/reason-nextjs/blob/master/src/Next.re)

```ocaml:github.com/mrmurphy/reason-nextjs/blob/master/src/Next.re
//
// Hooks
//

module Router = {
  type t = {
    // Current route. That is the path of the page in /pages
    pathname: string,
    // The query string parsed to an object. Defaults to {}
    query: Js.Dict.t(string),
    // Actual path (including the query) shown in the browser
    asPath: string,
  };

  type pushOptions = {
    shallow: bool,
    getInitialProps: option(bool),
  };
  [@bs.send]
  external push:
    (t, ~url: string, ~asUrl: string=?, ~options: pushOptions=?, unit) => unit =
    "push";

  [@bs.send]
  external replace:
    (t, ~url: string, ~asUrl: string=?, ~options: pushOptions=?, unit) => unit =
    "replace";

  type popStateContext = {
    url: string,
    as_: string,
    options: pushOptions,
  };
  [@bs.send]
  external beforePopState: (t, popStateContext => bool) => unit =
    "beforePopState";

  ();
  // Events are not wrapped at the moment. Feel free to contribute!
};

[@bs.module "next/router"] external useRouter: unit => Router.t = "useRouter";
```

`// Events are not wrapped at the moment. Feel free to contribute!`とある通り、完全ではないようです。
このプロジェクトを使うのは手の内の一つだったのですが、最終更新日時が古かったのでNext.jsのサンプルに載せています。

useRouter関連のコードをNext.reの末尾に貼り付けます（いいのか？）
package.jsonを確認するとMITライセンスのようです。
[MIT License 利用時の著作権表示箇所](https://qiita.com/khsk/items/f5dbd4a99c81f7bbe50a)
ライセンス全文と著作権表示を記載しないといけないようです。
相手先のリポジトリにライセンス全文と著作権表示がないので、
改変元のコードの場所と作者の名前と今の年、デフォルトの内容をRouterの上に記載しておきます。

```ocaml:Next.re
/*
 https://github.com/mrmurphy/reason-nextjs/blob/master/src/Next.re
 Code copyright 2020 Murphy Randle
 Code released under the MIT license
 https://opensource.org/licenses/mit-license.php
 */
```

これでuseRouterが呼べるようになりました。


### 実装
前に追加した　sample.reを変更し、フォームに入力してボタンを押したら遷移するようにしましょう。

```ocaml:sample.re
open Next;

[@react.component]
let make = () => {
  let (number, setNumber) = React.useState(() => "");
  let router = useRouter();
  let handleClick = _ => {
    let url = "/jumped?number=" ++ number;
    Router.push(router, ~url, ());
    ();
  };
  <div>
    <div> {ReasonReact.string({js|入力|js})} </div>
    <div>
      <input
        id="number"
        type_="text"
        name="number"
        value=number
        onChange={event => setNumber(ReactEvent.Form.target(event)##value)}
      />
    </div>
    <div>
      <button type_="button" onClick=handleClick>
        {ReasonReact.string({js|遷移|js})}
      </button>
    </div>
  </div>;
};

let default = make;
```

遷移先のページも作成します。

```ocaml:pages/jumped.re
[@react.component]
let make = (~number) => {
  <div> {ReasonReact.string(number)} </div>;
};

let default = make;

let getInitialProps = context =>
  Js.Promise.make((~resolve, ~reject as _) => {
    let number =
      switch (Js.Nullable.toOption(context##query##number)) {
      | None => "None"
      | Some(number) => number
      };
    resolve(. {"number": number});
  });

let inject:
  (
    Js.t('a) => React.element,
    {. "query": {. "number": Js.Nullable.t(string)}} =>
    Js.Promise.t({. "number": string})
  ) =>
  unit = [%bs.raw
  {| (cls, fn) => cls.getInitialProps = fn |}
];

inject(default, getInitialProps);
```

getInitialPropsはNext.jsの機能で、ページがレンダリングされる前に実行される内容を書き込みます。以下のフォーラムの質問を参考に実装しました。
[Struggling converting from Js object to reasonml record in nextjs getInitialProps](https://reasonml.chat/t/struggling-converting-from-js-object-to-reasonml-record-in-nextjs-getinitialprops/1902)


# APIを叩く

さぁ、APIを叩いてみましょう。ほとんどのサービスはWEB APIを叩くことで成り立っているので、ここが一番重要といっても過言ではありません。

今回は、livedoor 天気情報の[第三者向け気象データ提供サービス「Weather Hacks」](http://weather.livedoor.com/weather_hacks/)のお天気Webサービス（REST）を利用します。

## bs-fetch, bs-jsonの追加

[bs-fetch](https://github.com/reasonml-community/bs-fetch)
bs-jsonは[@glennsl/bs-json](https://github.com/glennsl/bs-json)を利用します。

ちなみに、ReasonMLではPromiseの使用は非推奨のようです。async/awaitも未対応なので、XMLHttpRequestを使いましょうとのことです。今回はPromiseとbs-fetchを利用します。

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Also, using promise is almost always wrong in your React/ReasonReact component! Since you’ve encouraged not thinking about cancelling them when a component unmounts.<br>Ever had a mysterious warning where you “set state on an unmounted component”? Now you know where that comes from! <a href="https://t.co/VcusHtGlsR">https://t.co/VcusHtGlsR</a></p>&mdash; Reason (@reasonml) <a href="https://twitter.com/reasonml/status/1238023504131317761?ref_src=twsrc%5Etfw">March 12, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

```
$ yarn add bs-fetch @glennsl/bs-json
```

bsconfig.jsonも変更します。

```json:bsconfig.json
{
  "name": "with-reasonml",
  "sources": [
    {
      "dir": "utils",
      "subdirs": true
    },
    {
      "dir": "components",
      "subdirs": true
    },
    {
      "dir": "pages",
      "subdirs": true
    },
    {
      "dir": "bindings",
      "subdirs": true
    }
  ],
  "bs-dependencies": ["reason-react", "bs-fetch", "@glennsl/bs-json"],
  "reason": { "react-jsx": 3 },
  "package-specs": { "module": "commonjs", "in-source": true },
  "suffix": ".bs.js",
  "bsc-flags": ["-bs-super-errors"],
  "refmt": 3
}
```
utilsフォルダを作成し、
bs-dependenciesに`"bs-fetch", "@glennsl/bs-json"`を追加します。

エラーが起きるのでpackage-specsのmoduleを`es6`から`commonjs`に変更します。
![ss_js_error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68992/2fbf787b-6377-5f72-0d80-6bb985661188.png)


## Api.reの作成

`utils/Api.re`を作成します。ページはルーティングの関係で`sanake_case.re`でしたが、他のファイルは基本的に`PascalCase.re`で書くのがルールのようです。他のcaseにしてもPascalCaseに変更されるとか。
[ReasonReact を使ってみる - 名前空間の話](https://qiita.com/mizchi/items/ef0db3f6c359d3bac171#%E5%90%8D%E5%89%8D%E7%A9%BA%E9%96%93%E3%81%AE%E8%A9%B1)

## 型の定義

JSONレスポンスをパースするために、レスポンスの型の情報を書きます。型を明示しない言語をずっと使ってきたので、なかなか慣れないですね。
[Weather Hacks - お天気Webサービス仕様]を見て、(http://weather.livedoor.com/weather_hacks/webservice)
[実際のレスポンス](http://weather.livedoor.com/forecast/webservice/json/v1?city=200010)を確認して書いていきましょう。

```ocaml:utils/Api.re(型部)
type linkNameData = {
  link: string,
  name: string,
};

type temperatureData = {
  celsius: string,
  fahrenheit: string,
};

type temperatureObj = {
  min: option(temperatureData),
  max: option(temperatureData),
};

type imageData = {
  width: int,
  link: option(string),
  url: string,
  title: string,
  height: int,
};

type forecastData = {
  dateLabel: string,
  telop: string,
  date: Js.Date.t,
  temperature: temperatureObj,
  image: imageData,
};

type locationData = {
  city: string,
  area: string,
  prefecture: string,
};

type copyrightData = {
  provider: array(linkNameData),
  link: string,
  title: string,
  image: imageData,
};

type descriptionData = {
  text: string,
  publicTime: Js.Date.t,
};

type weatherHacksData = {
  pinpointLocations: array(linkNameData),
  link: string,
  forecasts: array(forecastData),
  location: locationData,
  publicTime: Js.Date.t,
  copyright: copyrightData,
  title: string,
  description: descriptionData,
};
```
私がつまったところは、ネスト、配列、日付です。
ネスト -キーに対してオブジェクトが紐付くとき- は別のtypeに書きます。
配列のときはオブジェクト単位をネスト同様に書き、arrayでくくります。
日付はオブジェクトがJs.Dateで、型はJs.Date.tになるようです。

## デコーダの定義

型に合わせてJSONをデコードする部分を書きます。

```ocaml:utils/Api.re(デコーダ部)
module Decode = {
  let linkName = (data: Js.Json.t) =>
    Json.Decode.{
      link: data |> field("link", string),
      name: data |> field("name", string),
    };
  let temperature' = (data: Js.Json.t) =>
    Json.Decode.{
      celsius: data |> field("celsius", string),
      fahrenheit: data |> field("fahrenheit", string),
    };
  let temperature = (data: Js.Json.t) =>
    Json.Decode.{
      min: data |> optional(field("min", temperature')),
      max: data |> optional(field("max", temperature')),
    };
  let image = (data: Js.Json.t) =>
    Json.Decode.{
      width: data |> field("width", int),
      link: data |> optional(field("link", string)),
      url: data |> field("url", string),
      title: data |> field("title", string),
      height: data |> field("height", int),
    };
  let forecast = (data: Js.Json.t) =>
    Json.Decode.{
      dateLabel: data |> field("dateLabel", string),
      telop: data |> field("telop", string),
      date: data |> field("date", string) |> Js.Date.fromString,
      temperature: data |> field("temperature", temperature),
      image: data |> field("image", image),
    };
  let location = (data: Js.Json.t) =>
    Json.Decode.{
      city: data |> field("city", string),
      area: data |> field("area", string),
      prefecture: data |> field("prefecture", string),
    };
  let copyright = (data: Js.Json.t) =>
    Json.Decode.{
      provider: data |> field("provider", Json.Decode.array(linkName)),
      link: data |> field("link", string),
      title: data |> field("title", string),
      image: data |> field("image", image),
    };
  let description = (data: Js.Json.t) =>
    Json.Decode.{
      text: data |> field("text", string),
      publicTime: data |> field("publicTime", string) |> Js.Date.fromString,
    };
  let weatherHacks = (data: Js.Json.t) =>
    Json.Decode.{
      pinpointLocations:
        data |> field("pinpointLocations", Json.Decode.array(linkName)),
      link: data |> field("link", string),
      forecasts: data |> field("forecasts", Json.Decode.array(forecast)),
      location: data |> field("location", location),
      publicTime: data |> field("publicTime", string) |> Js.Date.fromString,
      copyright: data |> field("copyright", copyright),
      title: data |> field("title", string),
      description: data |> field("description", description),
    };
};
```
デコーダは型毎につくっていきます。

```ocaml:utils/Api.re(デコーダ部・抜粋)
module Decode = {
  let weatherHacks = (data: Js.Json.t) =>
    Json.Decode.{
      link: data |> field("link", string),
    };
};
```

抜粋・簡易化した上の内容だと、jsonを

```json:will_be_decoded.json
{
  "link": "will_be_decoded"
}
```
として、
`(data: Js.Json.t)` Js.Json型のdata(=responseのjson)を引数として、
`Json.Decode`を実施
`link: data |> field("link", string),`引数dataの"link"キーの値をstringとしてデコードし返り値weatherHacksのlinkの値にするというイメージ。

## Api kick

[bs-fetchの実装例](https://github.com/reasonml-community/bs-fetch/blob/master/examples/reason_examples.re)

```ocaml:utils/Api.re
let getWeather = locationId => {
  Js.Promise.(
    Fetch.fetch(
      "/api/forecast/webservice/json/v1?city=" ++ string_of_int(locationId),
    )
    |> then_(Fetch.Response.json)
    |> then_(obj =>
         obj
         |> Decode.weatherHacks
         |> (weatherHacks => Some(weatherHacks) |> resolve)
       )
    |> catch(_error => resolve(None))
  );
};
```

catch文がちょっと怪しい…ここはちょっと分かってないです。
`page/sample.re`で入力した数字（locationId）を文字列に変換して++で結合します。
返り値をoptionalにして、上手くいったらweatherHacksData型、エラーが起きたらNoneを返すつもりです。

## jumped.re記述

Apiを呼び出す関数を定義し、初期読込時に実行します。

```ocaml:pages?jumped.re(コンポーネント部)
[@react.component]
let make = (~number) => {
  let getWeather = _ => {
    Api.getWeather(number)
    |> Js.Promise.then_((response: option(Api.weatherHacksData)) => {
         switch (response) {
         | Some((response: Api.weatherHacksData)) => Js.log(response)
         | None => alert("An error has occurred!!")
         };
         Js.Promise.resolve();
       })
    |> ignore;
    ();
  };

  React.useEffect0(() => {
    getWeather();
    None;
  });

  <div> {ReasonReact.string(string_of_int(number))} </div>;
};
```

useEffect0はHooksのメソッドで、読込時に呼ばれます。

[React - 副作用フックの利用法](https://ja.reactjs.org/docs/hooks-effect.html)

>ヒント
>
>React のライフサイクルに馴染みがある場合は、useEffect フックを componentDidMount と >componentDidUpdate と componentWillUnmount がまとまったものだと考えることができます。

getWeatherの中でApiを叩き、パターンマッチングで分岐してます。あればresponeを`console.log`で出力、なければメッセージを`window.alert`で表示します。

alertの利用については、`bindings/Next.re`に記述します。
Jｓの機能なので(ですよね？)ファイルを分けたいところではあります。

```ocaml:bindings/Next.re
[@bs.val] external alert: string => unit = "alert";
```

これで一度動かしてみると、なんと！…CORSで失敗します。

```
Access to fetch at 'http://weather.livedoor.com/forecast/webservice/json/v1?city=200010' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
```

## CORS対策

プロキシ設定を作成します。

[Nextで特定のAPI リクエストをproxyする方法](https://qiita.com/onigiri_/items/d0c96662e1caeaf8db5f)

```console
$ yarn add express http-proxy-middleware
```

```javascript:server.js
/* eslint-disable no-console */
const express = require('express');
const next = require('next');
const {createProxyMiddleware} = require('http-proxy-middleware');

const port = parseInt(process.env.PORT, 10) || 3000;
const env = process.env.NODE_ENV;
const dev = env !== 'production';
const app = next({
    dir: '.', // base directory where everything is, could move to src later
    dev,
});

const handle = app.getRequestHandler();

let server;
app
    .prepare()
    .then(() => {
        server = express();
        server.use(
            createProxyMiddleware('/api', {
                target: 'http://weather.livedoor.com',
                pathRewrite: {'^/api': ''},
                changeOrigin: true,
            }),
        );

        // Default catch-all handler to allow Next.js to handle all other routes
        server.all('*', (req, res) => handle(req, res));

        server.listen(port, (err) => {
            if (err) {
                throw err;
            }
            console.log(`> Ready on port ${port} [${env}]`);
        })
    })
    .catch((err) => {
        console.log('An error occurred, unable to start the server');
        console.log(err);
    });
```

```
createProxyMiddleware('/api', {
    target: 'http://weather.livedoor.com',
    pathRewrite: {'^/api': ''},
    changeOrigin: true,
}),
```

でプロキシを定義します。
`/api`にアクセスすると`http://weather.livedoor.com`にアクセスするようにします。

```ocaml:utils/Api.re
let getWeather = locationId => {
  Js.Promise.(
    Fetch.fetch(
      "/api/forecast/webservice/json/v1?city=" ++ string_of_int(locationId),
    )
    |> then_(Fetch.Response.json)
    |> then_(obj =>
         obj
         |> Decode.weatherHacks
         |> (weatherHacks => Some(weatherHacks) |> resolve)
       )
    |> catch(_error => resolve(None))
  );
};
```
Apiのアドレス冒頭を`/api/`に置き換えます。

```json:package.json(scripts)
"scripts": {
  "dev": "NODE_ENV=development concurrently \"bsb -clean-world -make-world -w\" \"node server.js\"",
  "dev:reason": "NODE_ENV=development bsb -clean-world -make-world -w",
  "dev:next": "NODE_ENV=development node server.js",
  "build": "NODE_ENV=production bsb -clean-world -make-world && next build",
  "start": "NODE_ENV=production node server.js"
},
```

`next`コマンドでの起動から`node server.js`での起動に切り替えます。

`Ctrl+C`でwatchを止め、再度起動します。

```console
$ yarn dev
```

`http://localhost:3000/sample`にアクセスし、
`200010`を入力、ボタンをクリックすると…

![ss_ajax.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68992/01ac8b30-57fe-abc1-99e5-2e20045b3334.png)

取得できました。無事パースされているようです。

# 表示

JSONを取得・パースできたということで、いよいよ表示します。

```ocaml:pages/jumped.re(make部)
[@react.component]
let make = (~number) => {
  let (weather, setWeather) = React.useState(() => None);
  let getWeather = _ => {
    Api.getWeather(number)
    |> Js.Promise.then_((response: option(Api.weatherHacksData)) => {
         switch (response) {
         | Some((response: Api.weatherHacksData)) =>
           Js.log(response);
           setWeather(_ => Some(response));
         | None => alert("None")
         };
         Js.Promise.resolve();
       })
    |> ignore;
    ();
  };

  React.useEffect0(() => {
    getWeather();
    None;
  });

  switch (weather) {
  | Some((weather: Api.weatherHacksData)) =>
    <div>
      <div> {ReasonReact.string(weather.title)} </div>
      <div> {ReasonReact.string(weather.description.text)} </div>
      {weather.forecasts
       ->Belt.Array.map(day => {
           <div key={day.dateLabel}>
             <div> {ReasonReact.string(day.dateLabel)} </div>
             <div> {ReasonReact.string(day.telop)} </div>
             <div>
               {ReasonReact.string(Js.Date.toDateString(day.date))}
             </div>
             <img src={day.image.url} />
           </div>
         })
       ->React.array}
    </div>
  | None => <div> {ReasonReact.string(string_of_int(number))} </div>
  };
};
```

make内を上の様に書きます。気温も表示しようと思ったのですが、最高/最低のoption値の扱いがわからなかった(なぜかOption(Option a')になっている？)ので、消してます。
ポイントはコンポーネントのループ処理でしょうか。
[ReasonReact - A List of Simple Examples](https://reasonml.github.io/reason-react/docs/en/simple)

![ss_display.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68992/f7bf7456-6568-05ae-dcca-fca3762b2fce.png)

やったぜ。

# 補足

今回は遷移したかったので遷移しましたが、別にページ遷移する必要はない気がします。
ハッシュで同一ページで処理することも考えます。
また、`useEffect`で呼び出してますが`Next.js`のSSR用のメソッドがあるので、それが使えるようにしたいですね。
今回、あんまりReactiveではないですね。Next.jsの恩恵を受けたい。

# 感想

今回の内容だけあれば大抵のアプリはなんとかなりそう。強いて言えばスタイルの適用でしょうか。
Post時のJSON EncodeはDecode同様にやれば行けると思います。
ReasonMLはJS/TSで書くこれはどうやるんや、と検索をかけてもほとんど引っかからないので厳しいです。あっても英語なので、理解にはどうしても日本語より時間がかかります。
また、bindingが書けないとNext.jsの全機能は活かせないです。
有力なライブラリはbsバインドが作成されていますが、自前で用意するのは型の理解と時間が必要なので、それなりの経験を要すると思います。
力のある方はbinding/Next.reを埋めていって欲しいです。

また、今回Qiitaの記事書いてみて、すごい時間がかかるなぁ、という気持ちになった。
ReasonML、苦しいですが、型安全で頑張れば完成度の高いコードになるはずなので、興味のある方は是非取り組んでみてください。

# 参考文献
[Next.js](https://nextjs.org/)
[Reason](https://reasonml.github.io/ja/)
[Example app using ReasonML & ReasonReact components](https://github.com/vercel/next.js/tree/master/examples/with-reasonml)
[関数コンポーネントとクラスコンポーネント](https://ja.reactjs.org/docs/components-and-props.html#function-and-class-components)
[String & Character - Reason](https://reasonml.github.io/docs/ja/string-and-char)
[Next.js - Routing](https://nextjs.org/docs/routing/introduction)
[Next.js - useRouter](https://nextjs.org/docs/api-reference/next/router#userouter)
[mrmurphy/reason-nextjs - reason-nextjs/src/Next.re](https://github.com/mrmurphy/reason-nextjs/blob/master/src/Next.re)
[MIT License 利用時の著作権表示箇所](https://qiita.com/khsk/items/f5dbd4a99c81f7bbe50a)
[Struggling converting from Js object to reasonml record in nextjs getInitialProps](https://reasonml.chat/t/struggling-converting-from-js-object-to-reasonml-record-in-nextjs-getinitialprops/1902)
[第三者向け気象データ提供サービス「Weather Hacks」](http://weather.livedoor.com/weather_hacks/)
[bs-fetch](https://github.com/reasonml-community/bs-fetch)
[@glennsl/bs-json](https://github.com/glennsl/bs-json)
[ReasonReact を使ってみる - 名前空間の話](https://qiita.com/mizchi/items/ef0db3f6c359d3bac171#%E5%90%8D%E5%89%8D%E7%A9%BA%E9%96%93%E3%81%AE%E8%A9%B1)
[Weather Hacks - お天気Webサービス仕様](http://weather.livedoor.com/weather_hacks/webservice)
[React - 副作用フックの利用法](https://ja.reactjs.org/docs/hooks-effect.html)
[Nextで特定のAPI リクエストをproxyする方法](https://qiita.com/onigiri_/items/d0c96662e1caeaf8db5f)
[ReasonReact - A List of Simple Examples](https://reasonml.github.io/reason-react/docs/en/simple)
[Decoding Nested JSON Objects in ReasonML with bs-json](https://itnext.io/decoding-nested-json-objects-in-reasonml-with-bs-json-4cab75fbe308)

# アペンディクス

## getInitialProps

getInitialPropsは非推奨になったみたいです…。
[Next.js 9.3新API getStaticProps と getStaticPaths と getServerSideProps の概要解説](https://qiita.com/matamatanot/items/1735984f40540b8bdf91%E3%80%80)
`getInitialProps`を単純に`getServerSideProps`に置き換えるとエラーが出るので一旦getIntialPropsを使うことにします。

```
Error: page /jumped getServerSideProps can not be attached to a page's component and must be exported from the page. See more info here: https://err.sh/next.js/gssp-component-member
```
エラーの理由は https://err.sh/next.js/gssp-component-member にある通り、Next.js上で書き方に違いがあるから、とのようです。

`ReasonML getServerSideProps`で検索すると、[reasonml.orgのリポジトリにプルリクエストが発行されているの](https://github.com/reason-association/reasonml.org/pull/183)が見つかります。
ただ、`We went a completely different route though. Instead of using getServerSideProps, we do the following:`とある通り、`getServerSideProps`は使わない方針になったのかもしれません。ただ、`bindings/Next.re`への[追記 - Improve GetServerSideProps bindings](https://github.com/reason-association/reasonml.org/pull/183/commits/faaf78b8ba7168a96400f9ab03bf81f93548cfe5)はmergeされているようです。

[bindings/Next.re](https://github.com/reason-association/reasonml.org/blob/master/bindings/Next.re)

```ocaml:github.com/reason-association/reasonml.org/blob/master/bindings/Next.re
module GetServerSideProps = {
  module Req = {
    type t;
  };

  module Res = {
    type t;

    [@bs.send] external setHeader: (t, string, string) => unit = "setHeader";
    [@bs.send] external write: (t, string) => unit = "write";
    [@bs.send] external end_: t => unit = "end";
  };

  // See: https://github.com/zeit/next.js/blob/canary/packages/next/types/index.d.ts
  type context('props, 'params) = {
    params: Js.t('params),
    query: Js.Dict.t(string),
    req: Req.t,
    res: Res.t,
  };

  type t('props, 'params) =
    context('props, 'params) => Js.Promise.t({. "props": 'props});
};

module GetStaticProps = {
  // See: https://github.com/zeit/next.js/blob/canary/packages/next/types/index.d.ts
  type context('props, 'params) = {
    params: 'params,
    query: Js.Dict.t(string),
    req: Js.Nullable.t(Js.t('props)),
  };

  type t('props, 'params) =
    context('props, 'params) => Promise.t({. "props": 'props});
};

module GetStaticPaths = {
  // 'params: dynamic route params used in dynamic routing paths
  // Example: pages/[id].js would result in a 'params = { id: string }
  type path('params) = {params: 'params};

  type return('params) = {
    paths: array(path('params)),
    fallback: bool,
  };

  type t('params) = unit => Promise.t(return('params));
};
```

この方のプルリクエスト内の実装見て試してみたのですが、できなかったので保留します。