は地味に面倒
<select/>
は地味に面倒複数の選択肢の中から一つを選ばせる UI である<select/>
は便利なのでよく使われますが、SPA などを使った抽象化レイヤーに慣れていると地味に扱いが面倒です。というのも、<option/>
のvalue
は文字列だからオブジェクトをそのまま突っ込んだりできず、オブジェクトの ID やインデックスをとったりシリアライズ処理を書いて選ばれた値と欲しいデータのマッピングをするケースがよくあります。
// Reactの場合
const items = [
{
id: 1,
name: 'Foo'
},
{
id: 2,
name: 'Bar'
}
]
const MyComponent = () => {
const [selected, setSelected] = useState(null)
const handleChange = ev => {
setSelected(items.find(item => item.id === Number(ev.currentTarget.value)))
}
return (
<select value={selected?.id} onChange={handleChange}>
{items.map(item => (
<option key={item.id} value={String(item.id)}>
{item.name}
</option>
))}
</select>
)
}
選択肢がオブジェクトだけの場合は比較的シンプルですが、null
が混じってきたりオブジェクトではなく配列だった場合はさらに面倒になります。
こういった場合は<select/>
のラッパーコンポーネントを用意することが多いですが、その際に※1オススメなのが JSON value 方式です。これは単純に、<option/>
のvalue
にJSON.stringify
した値を使い、<select/>
のvalue
をJSON.parse
する、というものです。
※1 ... もちろんラッパーを用意しなくても使えます。
// Reactの場合
// MyOption.jsx
export const MyOption = ({ children, value }) => {
const serialized = useMemo(() => JSON.stringify(value), [value])
return <option value={serialized}>{children}</option>
}
// MySelect.jsx
export const MySelect = ({ children, value, onChange }) => {
const handleChange = ev => {
onChange(JSON.parse(ev.currentTarget.value))
}
const serialized = useMemo(() => JSON.stringify(value), [value])
return (
<select value={serialized} onChange={handleChange}>
{children}
</select>
)
}
// 使い方
const Page = () => {
const items = [
{
id: 1,
name: 'Foo'
},
{
id: 2,
name: 'Bar'
}
]
const MyComponent = () => {
const [selected, setSelected] = useState(null)
return (
<MySelect value={selected} onChange={setSelected}>
{items.map(item => (
<MyOption key={item.id} value={item}>
{item.name}
</MyOption>
))}
</MySelect>
)
}
}
React の Context のような値の伝播を使うこともできますが、実装が複雑になる上 straight forward な使い方ではなくなってしまいます。JSON value であれば基本的な HTML(JavaScript)の利用方法で使える上、どんなフレームワークやライブラリ(ライブラリを使わなくても)でも使えます。
<!-- Vueの場合(色々省略) -->
<script>
export default {
props: {
value: {}
},
computed: {
$_value() {
return JSON.stringify(this.value)
}
},
methods: {
handleChange(ev) {
this.$emit('change', JSON.parse(ev.currentTarget.value))
}
}
}
</script>
<template>
<select :value="$_value" @change="handleChange">
<slot />
</select>
</template>
当たり前ですが、この方法にもいくつかダウンサイド/注意点があります。
Symbol
/ネイティブオブジェクト/関数等のシリアライズできないものや、NaN
のように変換されてしまうものは値として利用できないパフォーマンスに関しては適切なキャッシュやメモ化をしていれば特に問題になることはないでしょう。2 つ目に関しても通常の利用であればそこまで問題になることはないですが、頭の隅にでも入れておく必要はあります。最後のものに関しては主に SSR や既にある HTML を JS でいじるときに気をつける必要が出てきます。