Remixが気になっていたので入門してみた

どうもこんにちは、たくびー(@takubii)です。

今回は最近話題になっているRemixについてチュートリアルを通して軽く触れてみました。
RemixはReactベースのSSR（サーバーサイドレンダリング）フレームワークでよくNext.jsと比較されます。
Next.jsに比べフレームワークの安定感やWeb標準に重きを置いている点など良いところが何点かあるので、人によってはRemixの方が好きという方もいると思います。

以下のチュートリアルを見ながら今回感じたことをつらつらと述べていきたいと思います。

https://remix.run/docs/en/main/start/tutorial

## 進め方

今回もNext.jsのチュートリアルを進めた時と同様にスクラッチでコンポーネントなど作りながら進めました。
インストール方法は以下のページに書いてあり、とても簡単に初期セットアップができました。

https://remix.run/docs/en/main/start/quickstart

## 所感

### チュートリアル全体を通して

Remixの基本を抑えながら簡単なアプリを作っていくシナリオでした。
作るコンポーネントも多くなく、Remixの理解を深めるためのものという方向性を感じられたので、こちらのチュートリアルに則って進めれば基本的な部分は理解できるようになります。

Remixはキャッシュなどがシンプルに作られているのでサイト構築している間のデータの取得等に意識を割かなくても良いところがとてもいいと感じました。
また、現在は基本的にSSRオンリーなので構築するサイトもSSRの利点を活かしたシンプルなものが出来上がると思います。

### 独自コンポーネントについて

まず最初に感じたのはRemix独自のコンポーネントが多数使われていたことに驚きました。
最初はどのコンポーネントが必須なのか等を以下のドキュメントのコンポーネント項目でそれぞれ調べながら進めました。

https://remix.run/docs/en/main/components/outlet

### データフローについて

Remixではデータの取得や渡し方などは大まかに決まりがあります。
以下の画像をご覧ください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/557065/4920a8cd-a25a-733c-7986-00f4b2bd0491.png)

コンポーネントではLoaderからデータを受け取り、Formを使ったActionでデータを他のコンポーネントに渡すのが一般的です。
Remixに慣れていないとこのあたりも最初は理解が難しいですが、チュートリアル上で何度もデータの受け渡しを行うので進めていくと理解が深まります。

下記のコードではそれぞれ`loader`関数と`action`関数を定義してデータの受け渡しを行なっています。
`loader`関数では前のページから送られてきたデータを元にさらにデータの取得などを行います。
`action`関数では`Form`コンポーネントの`post`メソッドの処理を記載しています。

```ts:contacts.$contactId.tsx
import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { Form, useFetcher, useLoaderData } from '@remix-run/react';
import { FunctionComponent } from 'react';
import invariant from 'tiny-invariant';
import { ContactRecord, getContact, updateContact } from '~/data';

export const loader = async ({ params }: LoaderFunctionArgs) => {
  invariant(params.contactId, 'Missing contactId param');
  const contact = await getContact(params.contactId);
  if (!contact) {
    throw new Response('Not Found', { status: 404 });
  }
  return json({ contact });
};

export const action = async ({ params, request }: ActionFunctionArgs) => {
  invariant(params.contactId, 'Missing contactId param');
  const formData = await request.formData();
  return updateContact(params.contactId, {
    favorite: formData.get('favorite') === 'true',
  });
};

export default function Contact() {
  const { contact } = useLoaderData<typeof loader>();

  return (
    <div id='contact'>
      <div>
        <img
          alt={`${contact.first} ${contact.last} avatar`}
          key={contact.avatar}
          src={contact.avatar}
        />
      </div>

      <div>
        <h1>
          {contact.first || contact.last ? (
            <>
              {contact.first} {contact.last}
            </>
          ) : (
            <i>No Name</i>
          )}{' '}
          <Favorite contact={contact} />
        </h1>

        {contact.twitter ? (
          <p>
            <a href={`https://twitter.com/${contact.twitter}`}>{contact.twitter}</a>
          </p>
        ) : null}

        {contact.notes ? <p>{contact.notes}</p> : null}

        <div>
          <Form action='edit'>
            <button type='submit'>Edit</button>
          </Form>

          <Form
            action='destroy'
            method='post'
            onSubmit={(event) => {
              const response = confirm('Please confirm you want to delete this record.');
              if (!response) {
                event.preventDefault();
              }
            }}
          >
            <button type='submit'>Delete</button>
          </Form>
        </div>
      </div>
    </div>
  );
}

const Favorite: FunctionComponent<{
  contact: Pick<ContactRecord, 'favorite'>;
}> = ({ contact }) => {
  const fetcher = useFetcher();
  const favorite = fetcher.formData
    ? fetcher.formData.get('favorite') === 'true'
    : contact.favorite;

  return (
    <fetcher.Form method='post'>
      <button
        aria-label={favorite ? 'Remove from favorites' : 'Add to favorites'}
        name='favorite'
        value={favorite ? 'false' : 'true'}
      >
        {favorite ? '★' : '☆'}
      </button>
    </fetcher.Form>
  );
};
```

### レイアウトについて

Remixでは基本的に親のレイアウトが適用されます。
`app/root.tsx`がトップのレイアウトになり、子コンポーネントはこのレイアウトが元になります。
レイアウトを親から引き継ぎたくない場合などは次のようなファイル名に`_`をつけることによってルーティングとレイアウトを変更する必要があります。`app/contacts.$contactId_.edit.tsx`
以下のドキュメントが参考になるので目を通すといいと思います。

https://remix.run/docs/en/main/file-conventions/routes

## 終わりに

チュートリアルは進めていくにつれRemixの独自使用が徐々にわかっていったのでとてもいいチュートリアルだと思いました。
使用している技術ベースはReactですが、Remix独特の技術が多く感じたのでしっかり使いこなすにはドキュメントをちゃんと読む必要があります。
ですが、基本はシンプルなので一度理解するとかなり使いやすいのでNext.jsよりもシンプルなものが使いたいといった要望には適っていると感じました。

最終的なコードは以下になります。
コード全体などご覧になりたい方は参考にしていただければ幸いです。

https://github.com/takubii/remix-contacts-demo

それでは今回はこのあたりで締めたいと思います。
ここまで読んでいただきありがとうございました。
また機会があればお会いしましょう。