12k
All articles

SolidJSを使った開発のベストプラクティス

SolidJSの細粒度リアクティビティ、シグナルスコープ、props処理、store活用、createResourceによる非同期パターンの落とし穴と対策を解説。

OpenReplay Team
OpenReplay Team
SolidJSを使った開発のベストプラクティス

SolidJSは、きめ細かいリアクティビティを通じてネイティブDOMに近いパフォーマンスを実現しますが、そのモデルはReactやVueから移行してきた開発者を混乱させるパターンを導入します。この記事では、実際のアプリケーションで最も重要なSolidJSのベストプラクティス、つまり微妙なバグを防ぎ、コードを予測可能に保つためのものを取り上げます。

重要なポイント

  • SolidJSコンポーネントはセットアップ関数として一度だけ実行される — リアクティビティはコンポーネントレベルではなく、シグナルレベルで機能する
  • シグナルの読み取りはリアクティブスコープ内に保つ:JSX式、createEffect、またはcreateMemo
  • エフェクトを通じて状態を同期するのではなく、関数またはcreateMemoで値を導出する
  • propsを分割代入しない — ゲッターチェーンを保持するためにmergePropssplitPropsを使用する
  • ネストされた状態にはcreateStoreを、非同期データフェッチにはcreateResourceを使用する

コンポーネントは一度だけ実行されることを理解する

これは、他のすべてが依存するメンタルモデルの転換です。Reactでは、状態が変更されるとコンポーネントが再レンダリングされます。SolidJSでは、コンポーネントはセットアップ関数として一度だけ実行されます。リアクティビティはコンポーネントレベルではなく、シグナルレベルで発生します。

つまり、これは誤りです:

function Counter() {
  const [count, setCount] = createSignal(0)
  const doubled = count() * 2 // 一度だけ実行される。更新されない。
  return <div>{doubled}</div>
}

修正方法は、シグナルの読み取りをリアクティブスコープ内に保つことです — JSX式、createEffect、またはcreateMemo:

function Counter() {
  const [count, setCount] = createSignal(0)
  const doubled = createMemo(() => count() * 2) // countをリアクティブに追跡
  return <div>{doubled()}</div>
}

SolidJSのリアクティビティパターン:シグナル、メモ、エフェクト

SolidJSのきめ細かいリアクティビティは、3つのプリミティブで構築されています。それぞれをいつ使用するかを知ることは、効率的なSolidJSコンポーネントを書く上で中心的な要素です。

  • createSignal — プリミティブ値とシンプルな状態用
  • createMemo — シグナルに依存する導出値用
  • createEffect — 副作用のみ用(DOM操作、サードパーティライブラリ)

最も一般的な間違いは、createEffectを状態の同期に使用することです:

// ❌ アンチパターン:導出にエフェクトを使用
createEffect(() => setFullName(`${firstName()} ${lastName()}`))

// ✅ 正しい方法:直接導出
const fullName = () => `${firstName()} ${lastName()}`

fullNameのような単純な関数が機能するのは、SolidJSが基になるシグナルを読み取るリアクティブスコープ内に現れるたびに、それを再評価するためです。createMemoに頼るのは、導出が高コストで結果をキャッシュしたい場合のみです。

createEffectは、Solidのリアクティブグラフの外部での作業、つまりチャートライブラリの初期化やDOM要素の命令的な更新などのために予約してください。より詳細については、memosと導出値の公式説明を参照してください。

propsを分割代入しない

SolidJSのpropsはゲッターによって支えられています。それらを分割代入すると、リアクティブな接続が壊れます:

// ❌ リアクティビティが壊れる
function User({ name }: { name: string }) {
  return <h1>{name}</h1>
}

// ✅ リアクティビティを保持
function User(props: { name: string }) {
  return <h1>{props.name}</h1>
}

デフォルト値が必要な場合や、消費するpropsと転送するpropsを分離したい場合は、mergePropssplitPropsを使用してください — どちらもゲッターチェーンを保持します。

条件付きレンダリングとリストレンダリングには制御フローコンポーネントを使用する

SolidJSのパフォーマンスパターンは、適切なレンダリングプリミティブの使用に依存しています。JSX内の生のJavaScriptロジックに頼るのではなく、リアクティブな条件とリストにはSolidの制御フローコンポーネントを優先してください。

// ✅ 条件付きレンダリング
<Show when={isLoggedIn()} fallback={<LoginPage />}>
  <Dashboard />
</Show>

// ✅ リストレンダリング — 更新間でアイテムの同一性を保持
<For each={posts()}>{(post) => <PostCard post={post} />}</For>

リストの位置が安定しているが、その位置の値が変わる可能性がある場合は、<For>の代わりに<Index>を使用してください。<For>は参照によってアイテムを追跡し、安定した同一性を持つオブジェクトの配列に最適ですが、<Index>は位置によってアイテムを追跡します。

Solidのリストレンダリングプリミティブについては、公式ドキュメントで詳しく読むことができます。

複雑な状態にはストアを使用する

シグナルは単一の値への変更を追跡します。その値が大きなネストされたオブジェクトの場合、更新は値全体を置き換えるため、そのシグナルのすべてのコンシューマーが変更に反応します。createStoreは、ネストされた状態に対してより適切に機能するプロパティレベルのリアクティビティを提供します:

const [state, setState] = createStore({ user: { name: "Alice" }, posts: [] })

// state.postsを読み取るコンポーネントのみがこれに反応
setState("posts", (p) => [...p, newPost])

複雑なミューテーションにはproduceを、既存のストアにサーバーデータをマージする場合はreconcileを使用してください。

非同期データの正しい扱い方

createEffect内でのフェッチは競合状態を引き起こす可能性があり、Suspenseときれいに統合されません。代わりにcreateResourceを使用してください:

const [posts] = createResource(() => fetch("/api/posts").then((r) => r.json()))

createResourceは、最初の引数としてオプションのソースシグナルを受け取ります。提供された場合、そのソースが変更されるたびにフェッチャーが再実行され、リソースは自動的にSolidの<Suspense>および<ErrorBoundary>コンポーネントと統合されます。

SolidStartアプリでは、プリロード関数と一緒にquerycreateAsyncを使用してください。プリロード関数は、ナビゲーションの意図(リンクのホバーなど)中に実行され、ナビゲーション中に再度実行されるため、コンポーネントがレンダリングされる時点でデータが準備できている状態にすることができます。

まとめ

SolidJSは、そのリアクティビティモデルに逆らうのではなく、それに沿って作業する開発者に報います。シグナルの読み取りをリアクティブスコープ内に保ち、エフェクトで同期するのではなく値を導出し、propsを分割代入せず、状態がネストされた構造を持つ場合はcreateStoreに頼ってください。これらのSolidJSリアクティビティパターンは恣意的なルールではありません — それらはきめ細かいリアクティビティがどのように機能するかの直接的な結果であり、それらに従うことで正確かつ高速なコンポーネントが生成されます。

よくある質問

SolidJSで導出値が更新されないのはなぜですか?

SolidJSコンポーネントは一度だけ実行されるため、リアクティブスコープ外のプレーン変数に割り当てられたシグナルの読み取りは、初期値のみをキャプチャします。導出をcreateMemoでラップするか、シグナルの読み取りを直接JSX内に配置してください。どちらも基になるシグナルが変更されるたびに再評価されるリアクティブスコープです。

プレーン関数の代わりにcreateMemoを使用すべきなのはいつですか?

プレーンな導出関数は、軽量な計算には問題なく機能します。なぜなら、SolidJSは消費するリアクティブスコープが実行されるたびにそれを再評価するからです。導出が高コストである場合や、複数のコンシューマーによって読み取られる場合にcreateMemoを使用してください。createMemoは結果をキャッシュし、依存関係が変更された場合にのみ再計算します。

SolidJSのForとIndexの違いは何ですか?

Forは各アイテムを参照によって追跡するため、安定した同一性を持つオブジェクトの配列に最適です。Indexは配列内のアイテムの位置によって追跡するため、位置が安定しているがその位置の値が変わる可能性があるリストに適しています。

他のシグナルから導出された状態を設定するためにcreateEffectを使用できますか?

できますが、すべきではありません。createEffectを使用して導出された状態を同期すると、不必要な中間更新が作成され、不具合が発生する可能性があります。代わりに、関数またはcreateMemoで値を導出してください。createEffectは、DOM操作、ログ記録、またはサードパーティライブラリとの対話などの真の副作用のために予約してください。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.