React Router v7 を remix よくわからんエンジニアが遊んでみた

# はじめに

React Router v7 がリリースされましたね。わたしが所属する JISOU コミュニティでも話題になっており遊んでみました！この記事では初学者ならではの遊んでみた気付きを共有します！

やったことは

- suspense を使ったローディング

です。

やらないことは

- remix の解説
- react router v7 の解説
- react router v6 との比較
- react router ベストプラクティス

です。

# プロジェクト作成

```bash
npx create-react-router@latest enjoy-suspense
```

たくさんファイル、ディレクトリを作成しますが app/routes ディレクトリにコンポーネントを作成して routes.ts を更新します。

# コンポーネント

PostsPage コンポーネントは Posts コンポーネントを内包しており、Posts コンポーネントは `use` を使って API から取得したデータを描画しています。全体を確認するには `コンポーネント全体` を確認してください。

```tsx
export default function PostsPage({ loaderData }: Route.ComponentProps) {
  let { postData } = loaderData;

  return (
    <div>
      <h1>Streaming example</h1>

      <Suspense fallback={<h2>🌀 Loading...</h2>}>
        <Posts postData={postData} />
      </Suspense>
    </div>
  )
}

export function Posts({ postData }: { postData: Promise<Post[]> }) {
  const posts = use(postData)

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>
          {post.title} ({post.body})
        </li>
      ))}
    </ul>
  );
}
```

<details><summary>コンポーネント全体</summary>

```tsx:app/routes/posts.tsx
import { Suspense, use } from "react";
import type { Route } from "./+types/posts";

type Post = {
  userId: number;
  id: number;
  title: string;
  body: string;
}

export async function loader({}: Route.LoaderArgs) {
  const promise = new Promise<Post[]>((resolve, reject) => {
    fetch('https://jsonplaceholder.typicode.com/posts')
      .then(response => {
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        return response.json()
      })
      .then(data => {
        setTimeout(() => {
          resolve(data); 
        }, 3000); 
      })
      .catch(error => {
        reject(error)
      })
  });

  return { postData: promise }
}

export default function PostsPage({ loaderData }: Route.ComponentProps) {
  let { postData } = loaderData;

  return (
    <div>
      <h1>Streaming example</h1>

      <Suspense fallback={<h2>🌀 Loading...</h2>}>
        <Posts postData={postData} />
      </Suspense>
    </div>
  )
}

export function Posts({ postData }: { postData: Promise<Post[]> }) {
  const posts = use(postData)

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>
          {post.title} ({post.body})
        </li>
      ))}
    </ul>
  );
}
```
</details>

# 面白いな :eyes: と思ったところ

- フレームワークが PostsPage に Props をわたしている

loaderData で API のレスポンスが見えるなら suspense でラップするぞ！と思ったのが失敗でした。あくまで `Suspense` はラップしたコンポーネントでフォールバックを起こして fallback のコンポーネントを表示するので `PostsPage` コンポーネントの中でやるべきですし、正確な Props はフレームワークが知っていることなので `export default` を使って外部に公開する必要があります。

```tsx
/* 間違った使い方 */
<Suspense>
  <PostsPage params={{}} loaderData={undefined} matches={[]} />
</Suspense>
```

:::note warn
Suspense の動作については誤った説明だと思います :pray: フォールバックの説明が難しいです。
:::

- loaderData が同期的に API のレスポンスをわたしている

わたしの理解では loaderData を使うと非同期で行ったサーバーコンポーネントの処理（ API リクエスト ）を同期的に取得してコンポーネントを描画する役割があります。コンポーネントは async / await をつけて定義できないのでここは「おおー、すごいな」と思いました（なぜそういった設計かは知らない）。

今回は `Suspense` を使いたいのでラップしたコンポーネントのなかでは `use` を使って一旦はレンダリングをフォールバックする必要があります。そのため `loader` では `Promise` を返す必要があり、かつオブジェクトで返さなければなりません。オブジェクトではなく `Promise` オブジェクトだと処理が終わるまで止まってしまいます :rolling_eyes: 

# おわりに

次のアクションとしては suspense を使った無限スクロールにチャレンジしたです。コンポーネント名やベストプラクティスについては目をつむってください :pray: