2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

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

やったことは

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

です。

やらないことは

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

です。

プロジェクト作成

npx create-react-router@latest enjoy-suspense

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

コンポーネント

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

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>
  );
}
コンポーネント全体
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>
  );
}

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

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

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

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

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

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

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

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

おわりに

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

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?