本日は iOS でお馴染みの Toggle Switch です。
code: github / $ yarn 1204
コンポーネントの使用感は以下の通り。props は全て optional で、型定義の通りボタン毎に指定が可能です。
<ToggleSwitch />
type Props = {
width?: number
height?: number
inactiveColor?: string
activeColor?: string
defaultChecked?: boolean
onChangeChecked?: (checked: boolean) => void
className?: string
}
useToggleSwitch
今回定義する Custom Hooks です。useEffect では、checked の変更に反応し props から受け付ける callback handler を叩きます。stateスキーマは単純で、checked のみを保持しています。
type State = {
checked: boolean
}
function useToggleSwitch(props: Props) {
const [state, update] = useState<State>(...)
const options = useMemo(...)
const nodeStyle = useMemo(...)
const baseStyle = useMemo(...)
const knobStyle = useMemo(...)
const inputStyle = useMemo(...)
const handleToggle = useCallback(...)
useEffect(...)
return {
state,
nodeStyle,
baseStyle,
knobStyle,
inputStyle,
handleToggle
}
}
Optional Injection
State とは別に、Custom Hooks 内部で「Options」を保持、幅・高さ・色を変更可能にしています。Optional なので、規定値を生成する defaultOptions
関数を定義します。
const defaultOptions = (): Options => ({
width: 50,
height: 30,
inactiveColor: '#eee',
activeColor: '#4ed164'
})
useMemo で memoize しつつ、規定値と props をマージする処理を行うことで、ロジックに必要な設定値を確保します。lodash.merge
を利用することで、面倒な undefined
マージ処理を簡略化することができます。
const options = useMemo(
(): Options =>
merge(defaultOptions(), {
width: props.width,
height: props.height,
inactiveColor: props.inactiveColor,
activeColor: props.activeColor,
onChangeChecked: props.onChangeChecked
}),
[
props.width,
props.height,
props.inactiveColor,
props.activeColor,
props.onChangeChecked
]
)
useState による値保持との違いは**「props による影響を初期限定にするか、継続的に影響を受けるか」**です。useState で確保された state は、初期マウント時以降、props の変更を受け付けません。変更するためには同時に生成された更新関数を利用する必要があります。Custom Hooks を利用する Component では、以下の様に Optional Injection を受けます。
const View = (props: Props) => {
const {
state,
nodeStyle,
baseStyle,
knobStyle,
handleToggle
} = useToggleSwitch({
width: props.width,
height: props.height,
checked: props.defaultChecked,
inactiveColor: props.inactiveColor,
activeColor: props.activeColor,
onChangeChecked: props.onChangeChecked
})
return (
<div className={props.className} style={nodeStyle}>
<span className="base" style={baseStyle} />
<span className="knob" style={knobStyle} />
<input
type="checkbox"
onChange={handleToggle}
checked={state.checked}
/>
</div>
)
}
不要な汎用化はNG
今日のサンプルは汎用化のためのテクニックとして、Optional Injection を紹介しました。プロダクトコードでこの様な汎用化が常に必要とは限りません。汎用化を必要最小限に留めることも時として必要です。