SolidJS君さぁ...
Reactとは違い、state
やprops
を分割代入すると、reactivityが失われます。どういうことかと言うと、
const Component = (props) => {
const { isLoading } = props
return (
<Show when={!isLoading} fallback={<div>Loading...</div>}>
<div>Some Content</div>
</Show>
)
}
こうやってisLoading
を分割代入すると、値が変わっても表示が切り替わらなくなります。
createSignal
やcreateStore
によって作成された状態に関しても同じです。
なぜなのか
公式ドキュメントのこのページの2番目のポイントに詳しく書いてありますね。
個人的な解釈をざっくりと書きます。
まず分割代入とは
const { isLoading } = props
というのは言い換えると、isLoading
という名前の変数にprops.isLoading
の値をアサインしているということです。
const isLoading = props.isLoading
と書くとわかりやすいですね。
自分は、「isLoading
という変数は、props.isLoading
の一時的なエイリアスでしかない」と解釈しています。
更新されるのはpropsの値のみ
新しい値が渡された時、props.isLoading
の値が更新されるのであって、isLoading
というただのエイリアスが直接更新されるわけではありません。
関数が一度しか実行されないのが理由
//この関数は一度しか実行されないから、
const Component = (props) => {
const { isLoading } = props //isLoadingという変数に値がアサインされるのは最初にここでだけ。
return (
//isLoadingというエイリアスの値はずっと変わらないからビューも変化しない。
<Show when={!isLoading} fallback={<div>Loading...</div>}>
<div>Some Content</div>
</Show>
)
}
Reactなら再レンダリングの度に関数が上から下まで実行されるので、分割代入もその度に行われますが、SolidJSはそもそも再レンダリングが起きません。
props.isLoading
の値が変わってもisLoading
という「一時的なエイリアス」にアサインし直すことはありません。
Reactivityを保持するためには
さっきのページによると
- jsxの中
- リアクティブな関数(
createEffect
など)の中
から直接props
やstate
にアクセスすれば、値は再評価されるとあります。
const Component = (props) => {
return (
// jsxの中で直接props.isLoadingにアクセスしているからちゃんと更新される。
<Show when={!props.isLoading} fallback={<div>Loading...</div>}>
<div>Some Content</div>
</Show>
)
}
いちいちprops.
なんて書きたくないよ!
たしかに。
幸い解決法はあります。
関数で包んであげる
大事なのは、jsx(もしくはリアクティブな関数)の中で値にアクセスすることなので、関数で包んで、その関数をjsx内で呼び出せば解決します。
まあこれは分割代入とは違いますが、一つの値をprops
から取り出したい時に使えます。
const Component = (props) => {
const isLoading = () => props.isLoading
return (
<Show when={!isLoading()} fallback={<div>Loading...</div>}>
<div>Some Content</div>
</Show>
)
}
実際にprops.isLoading
にアクセスしているのは、jsx内でisLoading
という「関数」を呼んでいる時なので値はちゃんと更新されます。
少しまとめます。
//jsx内でprops.someValueにアクセスしているので⭕
return (
<div>{props.someValue}</div>
)
//jsx外でprops.someValueにアクセスしているので❌
const { someValue } = props
return (
<div>{someValue}</div>
)
//関数を通して、jsx内でprops.someValueにアクセスしているので⭕
const someValue = () => props.someValue
return (
<div>{someValue()}</div>
)
ライブラリを使う
最初から言えよって感じですが。
destructureという、solidjs-communiyの方々が開発したライブラリです。(2022/6/10時点ではexperimental)
使い方はとってもシンプル。
const Component = (props) => {
const { isLoading } = destructure(props)
return (
<Show when={!isLoading()} fallback={<div>Loading...</div>}>
<div>Some Content</div>
</Show>
)
}
注意すべき点は、さきほどの例と同じくisLoading
は関数であるということです。
このライブラリに関しては別記事で詳しく解説するつもりですが、待てない方は公式ドキュメントを読んでください。
props
を2つに分けたい
「props
の一部だけを子コンポーネントに渡したい」という場合、分割代入を使いがちですよね。
const Component = (props) => {
const { title, ...childProps } = props
return (
<>
<h1>{title}</h1>
<Child {...childProps} />
</> )
}
こういった場合に便利なのがsplitProps
です。
const Component = (props) => {
const [local, childProps] = splitProps(props, ['title'])
return (
<>
<h1>{local.title}</h1>
<Child {...childProps} />
</>
)
}
デフォルトの値を設定したい
これも普段は分割代入を使いますよね。
const Component = ({ title = 'default title', ...props }) => {
return (
<h1>{title}</h1>
)
}
こういった場合にはmergeProps
を使います。
const Component = (props) => {
props = mergeProps({title: 'default title'}, props)
return (
<h1>{props.title}</h1>
)
}
結論
分割代入ができないという制約を面倒くさいと思うかは人次第ですが、個人的にはそこまで大きな問題だとは捉えていません。
実を言うとprops.
と書くのは、値がprops
からの物だとひと目で分かるの好きですし。
クソデカオブジェクトをまるっと分割代入して、変数名が縦長に並ぶよりprops.
と書くほうが好みです。
最悪destructureを使えばいいですしね。ちなみにdestructureがbuilt-inではない理由としては、「みんなそんなに分割代入しないでしょ」とのことです。ほんまか?
なにはともあれ、SolidJSでは分割代入しないでね!というお話でした。