RemixでOutletを使ったらinputの中身が更新されない?
ハマったこと
Remix(React Router v6)のOutlet
とダイナミックルーティングを組み合わせたとき、ページをDataLoader
でデータを取得しても、input
やtextaria
の初期値が更新されない。
解決策
input
やtextaria
にページ切り替え前後で異なるkey
を割り当てる。
具体的な状況
ディレクトリ(routes配下のみ抽出)
app/routes
├── index.tsx
├── posts
│ ├── $slug.tsx
│ ├── admin
│ │ ├── $slug.tsx
│ │ ├── index.tsx
│ │ └── new.tsx
│ ├── admin.tsx
│ └── index.tsx
└── test.tsx
以下は今回話をする上で、重要なコンポーネントの概要です(相当簡略化しています)。
// DataLoaderの返り値
{
posts: [
{
post: '記事1',
title: '記事のタイトル1',
markdown: '内容'
}
]
}
<>
{ /** 記事のリンクのリスト **/ }
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link to={post.slug}>
{post.title}
</Link>
</li>
))}
</ul>
<main>
// 配下の$slug.tsxを表示するOutlet
<Outlet />
</main>
</>
// DataLoaderの返り値
{
post: {
post: '記事1',
title: '記事のタイトル1',
markdown: '内容'
}
}
<form method="put">
<input
type="text"
name="title"
defaultValue={post.title}
/>
<input
type="text"
name="slug"
label="Post Slug"
defaultValue={post.slug}
/>
<textarea
name="markdown"
label="Markdown"
defaultValue={post.markdown}
/>
<button type="submit">Create Post</button>
</form>
以下の画像はhttp://localhost:3000/posts/admin/90s-mixtape
にアクセスした際の表示です。
左は記事のリストであり、admin.tsxで描写されます。
右は記事の編集フォームで、記事のリストから$slug.tsx
に渡されたparams
にも基づいて記事を検索し、現在の記事情報を表示します。
今回の問題は、一度記事を選択してから、別の記事に切り替えた際に発生しました。上記の画像からURLや記事のタイトルなどは変化しても、textaria
の中身は変化がないことがわかります。console.log
でDataloaderの出力が変化していることは確認しましたが、一部の要素が変化しません。useState
を使って各input
やtextaria
の状態を管理しても状況は変わらず…編集する記事を切り替えているのだから、全ての入力欄が変化してほしいところです。なお、textaria
以外の入力欄も変更を加えると、それ以降変化しなくなりました。
勘違いしていたこと
ネステッドルーティングでは、URLが変更されると新しいOutlet
要素がSSRで描画され、変更前のOutlet
要素は一切捨てられるものだと考えていました。しかし、実際はSuspend
などを利用し、Reactのルールに則っているため、基本的には前のOutlet
の状態を引き継ぐようで、今回input
やtextaria
が更新されない理由はここにありました。
状態をリセットして再描画したい要素にはkeyを明示的につける
以前の状態を切り捨て1から描画させたいときは、リストでなくても明示的にkey
を指定し、その変化でReactに全く別の要素になったことを示しましょう。以下のようにすることで、ページを切り替えDataLoaderの値が変化したときにkey
も変化するため、1からinput
やtextaria
を描写させることができます。
// 記事のユニーク値+入力欄のname
<form method="put">
<input
// タイトル用のキー
key={post.slug + "title"}
type="text"
name="title"
defaultValue={post.title}
/>
<input
// slug用のキー
key={post.slug + "slug"}
type="text"
name="slug"
label="Post Slug"
defaultValue={post.slug}
/>
<textarea
// Markdown用のキー
key={post.slug + "markdown"}
name="markdown"
label="Markdown"
defaultValue={post.markdown}
/>
<button type="submit">Create Post</button>
</form>
全ての入力欄が変化していることが読み取れます。
最後に
思わぬところでハマりだいぶ時間を使ってしまいました。RemixやNext.jsなどの便利なライブラリ、React18の新機能など色々新しいものを学ぶことも大事ですが、基礎的なところを疎かにしてはいけないですね…
参考資料
React.jsの地味だけど重要なkeyについて
React.js and Dynamic Children - Why the Keys are Important