💡

Remixの流儀

2022/08/31に公開

初めに

Remixをやるにあたって知っとくと便利というものを書いています。長いので、少しずつ追加していく予定です。新しい機能等も解説しています。リリースされてすぐに更新という訳にはいきませんが、たまに見てくださると更新があるかもしれません。

CHANGELOG

2023/03/06

  • v1.13.0で追加された機能について纏めました
  • 「Remixのバージョンアップで追加された機能の紹介」という項目を追加しました
  • 「状態管理ライブラリについて」という項目を追加しました。
  • useLoaderDate を使用する場合 Date 型が stringになってしまう事を追記
  • Remix v1.6.5 以前のバージョンを使用している場合は更新することをお勧めするという文章を追加
  • 「状態管理を行う際は ClientOnlyで囲ってもいいと思う」という見出しのタイトル及び内容を削除
    主に執筆時に私自身が理解できていなかっただけで、SSRの魅力を潰してしまっていると思ったため削除しました。

2023/01/16

  • useFetcher.dataの型を推論できるようになりました。のサンプルでloaderよりもactionが一般的な為コードを修正

2022/11/26

  • 「LocalStorageへのアクセス」の項目をSSR時の心構えなどに移動しました(Remixと直接関係がない為)
  • v1.7.4で変更された機能の紹介を追加しました

2022/9/2

  • VSCodeなどで自動インポートを使う際の注意点を追加
  • loaderの戻り値で Remix v1.6.5から使える新しい書き方を追加しました

Remixのバージョンアップで追加された機能の紹介

v1.13.0で変更された機能の紹介

PostCSSがビルドイン サポートになりました

私はいまいちpostcssを使ったことが無いので説明できないのですが、以下の設定をすることで有効にできます。

remix.config.js
module.exports = {
  future: {
    unstable_postcss: true,
  },
};
postcss.config.js
// postcss.config.js
module.exports = {
  plugins: [/* your plugins here! */],
  presets: [/* your presets here! */],
};

TailwindCSS がビルドイン サポートになりました

PostCSSがサポートされたってことは...って感じでとても嬉しいアップデートの一つですね!今まではpackages.jsonにコマンドを書く必要があって、少し手間がかかっていましたが、これを使うと pnpm remix dev といったコマンドだけで動作するようになります。

使用方法

tailwindcss のインストール

pnpm install -D tailwindcss
pnpm exec tailwindcss init
app/tailwind.css
@tailwind base;
@tailwind components;
@tailwind utilities;
remix.config.js
module.exports = {
  future: {
    unstable_tailwind: true,
  },
};
app/routes/root.tsx
import stylesheet from "~/tailwind.css";

export const links: LinksFunction = () => [
  { rel: "stylesheet", href: stylesheet },
];

他にもRemix v2の為の新しいv2ルーティングに関する更新もあるようですが、私はまだ使ったことが無いので割愛させて頂きます。詳しく知りたいという方はGitHubのリリースをご覧ください。

v1.7.4で変更された機能の紹介

useFetcher.dataの型を推論できるようになりました🎉

v1.6.5で行われた loaderなどの型推論と似た感じに書けますね
詳しくはリリースノートを見てみてください!

export async function loader(args: LoaderArgs) {
  return json({
    user: {
      name: "Chance",
      twitter: "@chancethedev",
      age: 36,
    },
  });
}

function SomeComponent() {
  let fetcher = useFetcher<typeof action>();
  if (fetcher.data) {
    let userName = fetcher.data.user.name; // string
    let userAge = fetcher.data.user.age; // number
  }
}

VSCodeなどで自動インポート使う場合は注意を

例えばですが、useLoaderData という loader で取得した値を取得するための関数があるのですが、こちらをインポートしようとしたときこんな感じになります。remix関係で色んなパッケージがありますね。
多くの場合においてこの関数はページなどで使うと思うので、@remix-run/react からインポートするのが正しいのですが、誤ってremix-utils等からインポートすると型等が異なるため、あれ思ってた挙動と違うということが起きるので注意しましょう。

loaderの戻り値の型

Remixには素晴らしい機能としてloaderという関数があります。ページ内に以下のようなコードを入れてみましょう

※2022/9/2追記 Remix v1.6.5からどうやら少し書き方を変えるだけで別の関数に分ける必要がなくなったようです。前から便利だったloaderがこれによりより一層使いやすくなりましたね!詳しい詳細はリリースノートをご覧ください

Remix v1.6.5からの書き方

こちらには一応metaを使用する場合の例も記述しておきます

import { json, LoaderArgs, MetaFunction } from "@remix-run/node"
import { useLoaderData } from "@remix-run/react"

export const meta: MetaFunction<typeof loader> = ({data}) => {
  return {
    title: data.status
  }
}

export async function loader(args: LoaderArgs) {
	return json({status: 'OK'})
}


export const Index = () => {
	const data = useLoaderData<typeof loader>()
	return (
		<div>status: {data.status}</div>
	)
}
Remix v1.6.5以前のバージョン
export const loader: LoaderFunction = () => {
    return json({status: 'OK'})
}

const Index = () => {
	const data = useLoaderData()
	return (<div>status: {data.status}</div>)
}

export default Index

おそらく画面にはstatus: OKと出ることでしょう。このloader関数内で書いたものはserver側で実行される物になります。しかし useLoaderData() で受け取ったデータに型がありません。これの解決策は公式ドキュメントにも書いてありますが、以下のようにコードを変更することです。

type LoaderData = Awaited<ReturnType<typeof getLoaderData>>;

async function getLoaderData() {
	return {status: 'OK'}
}

export const loader: LoaderFunction = async ({ request }) => {
	const res = await getLoaderData(request);
	return json(res);
};

const Index = () => {
	const data = useLoaderData<LoaderData>()
	return (<div>status: {data.status}</div>)
}

export default Index

データの取得部分を別の関数に分け、予め戻り値を型として作成しておくことで型安全なデータの取得ができるようになります。

コンポーネントからサーバー側にアクセスする

例えば、ユーザーのIDをPropsで受け取って、そこからユーザーを取得する場合などがこれに当たります。

解決に使うもの

  • useFetcher
  • action

さて、新しいものがどんどん出てきますが、まずは action からご紹介します。actionは先ほど紹介した loader と似た機能を持っています。loaderが主にgetメソッドを受け取るのと違い action は主に post メソッドなどを受け取ることができます。
実行時の優先順位としては loader < action です。
しかし、action一つだけでは一つの機能しか持たせることができません。
そこで今回は routes/api/user.tsのような感じにファイルを作ってみます。

user.ts
export const action: ActionFunction = async ({ request }) => {
    const formData = await request.formData();
    const intent = formData.get("intent");
    switch (intent) {
      case 'createUser': {
        // ユーザーの作成処理
      }
      case 'deleteUser': {
        // ユーザーの削除処理
      }
    }
}

コードを見てもらえばなんとなくわかる通り、intentを渡すことで動作を増やしています。
さて、では実際にこのactionを実行するためのファイルを作成します。

createUser.tsx
export const CreateUser = () => {
    const [createUser, setCreateUser] = useState(null)
    const fetcher = useFetcher()
    useEffect(() => {
        fetcher.submit({intent: 'createUser'}, {action: '/api/user', method: 'post'})
    }, [])

    useEffect(() => {
        if (!fetcher.data) return
        setCreateUser(fetcher.data)
        console.log(fetcher.data)
    }, [fetcher.data])
    return (
    <div>何か</div>
    )
}

これでこのコンポーネントを呼び出せばユーザーの作成処理が実行されるようになりました。
一応今回は例としてfetcher.dataの受け取りまでやってあります。fetcher.dataはReactQueryのようにisLoadingなどでアクセスできるわけではない(似たのはあった気がします?)ので、useEffectで更新があったら確認するようにするのが良いです。個人的にはfetcherの使い方が一番理解できなくて大変でした。ちなみにactionはデフォルトだと現在のパスが入ります。

また、/単体にしたい場合は/?indexと指定します。詳しい理由に関しては公式ドキュメントに記載がありますが要略すると、以下のようなファイル構造があるとします。

.
└── routes
    ├── $userId.tsx
    └── index.tsx

index.tsxのアクセス方法は/
$userId.tsxのアクセス方法は/12345
です。これらを呼び出したりする際にindex.tsxを呼び出してるのか、$userId.tsxを呼び出してるのか分かりにくい!ということで?indexをつけているようです

状態管理ライブラリについて

valtio(状態管理ライブラリ)をSPAと同じノリで使うのは辞めよう

私自身やってから気づいたことなのですが、どうもサーバーサイド側に状態が残ってしまうようで、たとえばユーザーAがログインし、それを root.tsx などで状態にセットすると次に別のユーザーがサイトを見た際ユーザーAとして描画が行われてしまいます。 Remix-Auth等のライブラリを使用している場合はそこまで大事にはならない場合もあると思いますが、ユーザーのプロフィール等を不正に見れてしまう可能性を孕んでいるので気を付けましょう(1敗)。

お勧めの状態管理ライブラリ

jotaiがお勧めです。Zennでも作者様が記事を書いていらっしゃるので日本語の情報が豊富です。また、SSR時に初期値を入れておいて、クライアント側で状態に目的のデータをセットするということができ非常に便利です。

一応SSR時の使い方を記載しておきます。

root.tsx
export default function App() {
  useHydrateAtoms([
    [useSessionState, undefined],
    [useHoge, { age: 'huga' }],
  ]);
  const setSession = useSetAtom(useSessionState);
  setSession({})

  ... // Outlet等を書く
}

useHydrateAtomsはまず、Arrayを受け取るのですが、そこに更に[atom, atomの初期値]という形のArrayを入れたものを渡します。そうすることでSSR時に状態が保存されて他のユーザーにも見えてしまうといったことが防げます。私自身どこでこれを動作させるべきなのかよくわかりませんが、root.tsxで実行しとけば問題ないと思います。

外部ライブラリ(Chart.js等)でエラーが出る場合

これは多くの場合チャート系で発生することが多いと思います。主にサーバーサイドでレンダリングする際に window 等といったものが使用できないために発生します。これらを解決するには remix-utils というライブラリを使用します。

remix-utils はRemixの開発者の一人である sergiodxa 様が作成しているライブラリで、クライアントのみで特定のコンポーネントを描画させるための機能などを提供してくれます。
使い方は以下の通りです。

import { ClientOnly } from "remix-utils";

export default function Component() {
  return (
    <ClientOnly fallback={<SimplerStaticVersion />}>
      {() => <ComplexComponentNeedingBrowserEnvironment />}
    </ClientOnly>
  );
}

ClientOnlyというコンポーネントで対象のコンポーネントを囲い、アロー関数で呼び出す形にします。これでクライアントのみでコンポーネントが描画されるようになり、問題を解決できます。

余談: SSRの時の心構えや便利な物

ここまで読んでいただきありがとうございます!Remixに直接関係がある話題はここで終わりになります。 お疲れさまでした。続きはSSRの時に覚えとくといいかも?程度の事を適当に書いているコーナーです。良ければお読みください。

LocalStorageへのアクセス

SSRではSPAと違ってそのままでは大抵LocalStorageにアクセスできません。そのため、よくある方法でwindowがundefinedか確認するのですが、毎回これを書くのは正直手間です。
なので、以下のようにラップしてしまいます。

この関数には以下のような機能があります

  • 指定したkeyで値が無かった場合は第二引数のデフォルト値を返す
  • 引数にjson=trueを渡すことでLocalStorageに保存した際にstringにしたjsonをobjectに戻した状態で手に入れれる
  • 関数呼び出し時にgenericsを用いることで戻り値の型を定義できる
export const getLocalStorage = <T>(
	key: string,
	defaultValue: T,
	json: boolean = false,
): T => {
	if (typeof window !== "undefined") { // ここ
		const item = window.localStorage.getItem(key);
		return item ? (json ? JSON.parse(item) : item) : defaultValue;
	}
	return defaultValue;
};

この関数の使い方は以下のようになります

getLocalStorage<{name: string, password: string}>('account', {name: 'default', password: 'defaultPassword'})

無駄だと思うところがあったらコメント貰えると嬉しいです。

認証情報はCookieに乗せると楽

どっちに認証情報を置くかは人にもよると思いますが、SSRにおいてはLocalStorageなどにトークンなどを置くよりCookieに置く方が断然楽だと思います。理由は今回の記事を読んだ方ならある程度察しが付くと思います。

サーバーサイドで使う値をlocalstorageになるべく持たせない

あんまりやらないと思いますが、LocalStorageにサイトのURLがあるとして、そのURLを取得し、OGPに表示するというのはかなり難しいです。
災厄Cookieに値を持たせることで回避することも出来ますが、Cookieはリクエストを行うたびにその内容が送られてしまい、そのサイトを利用しているユーザーのネットを圧迫する原因になります。

拡張子に.clientや.serverを使う

Remixにはclientサイドで読み込むスクリプトにはhoge.client.ts、serverサイドで読み込むスクリプトには fuga.server.ts といった風な感じに名前を変えることでクライアント側からは呼び出せなくなったりする機能があります。例として挙げると postgres などのセッションを定義するのはserver, apiの通信を行うものは clientみたいな感じですです。
ぱっと見でどっちで使うものか分かりやすくなるのもそうですが、今回の記事のようにLocalStorageなどにアクセスするスクリプトなどを誤って実行しないように出来ます。

最後に

ここまで読んでくださった方、ありがとうございます!そしてお疲れ様です!この記事を読んで少しでもRemixに興味を持ってもらえると嬉しいです。

参考

https://remix.run/docs/en/v1

Discussion