Created
June 16, 2025 05:51
-
-
Save manabuyasuda/4ff610b65639319862f157637e95b207 to your computer and use it in GitHub Desktop.
selectタグの幅を動的に計算するカスタムフック
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use client'; | |
import { useLayoutEffect, useRef, useSyncExternalStore, useCallback } from 'react'; | |
/** | |
* selectタグの幅を動的に計算するカスタムフックです。 | |
* デフォルトではoptionタグにある最長のテキストに基づいて幅を計算しますが、これを上書きします。 | |
* @param selectRef - selectタグへの参照 | |
* @param currentValue - 現在選択されている値 | |
* @returns 計算された幅(ピクセル単位)または null(SSR時) | |
* | |
* @example | |
* ``` | |
* function MySelect() { | |
* const selectRef = useRef<HTMLSelectElement>(null); | |
* const [value, setValue] = useState(''); | |
* | |
* // フックを使用して幅を取得 | |
* const width = useSelectWidth(selectRef, value); | |
* | |
* return ( | |
* <select | |
* ref={selectRef} | |
* value={value} | |
* onChange={(e) => setValue(e.target.value)} | |
* style={width ? { width: `${width}px` } : undefined} | |
* > | |
* <option value="">選択してください</option> | |
* <option value="1">短いテキスト</option> | |
* <option value="2">とても長いテキストが入る場合</option> | |
* </select> | |
* ); | |
* } | |
* ``` | |
*/ | |
export function useSelectWidth( | |
selectRef: React.RefObject<HTMLSelectElement | null>, | |
currentValue: string, | |
) { | |
const widthRef = useRef<number | null>(null); | |
// ハイドレーションエラーを防止する | |
const selectWidth = useSyncExternalStore( | |
// ストアの変更を監視する関数 | |
// この実装では幅の変更を外部から通知する必要がないため、空の関数を返す | |
() => () => {}, | |
// 現在の幅を取得する関数 | |
// クライアントサイドでのみ実行され、現在の幅を返す | |
() => widthRef.current, | |
// サーバーサイドでの初期値を返す関数 | |
// SSR時は常にnullを返し、ハイドレーション時の不一致を防ぐ | |
() => null, | |
); | |
// 幅を計算する関数をメモ化 | |
const calculateWidth = useCallback(() => { | |
if (!selectRef.current) return; | |
const select = selectRef.current; | |
const selectedOption = select.options[select.selectedIndex]; | |
if (selectedOption) { | |
const computedStyle = window.getComputedStyle(select); | |
// 不可視なspanタグを生成する | |
const tempSpan = document.createElement('span'); | |
tempSpan.style.visibility = 'hidden'; | |
tempSpan.style.position = 'absolute'; | |
tempSpan.style.whiteSpace = 'nowrap'; | |
// selectタグのスタイルを継承する | |
tempSpan.style.fontSize = computedStyle.fontSize; | |
tempSpan.style.fontFamily = computedStyle.fontFamily; | |
tempSpan.textContent = selectedOption.text; | |
// ページに出力して幅を計測する | |
document.body.appendChild(tempSpan); | |
const textWidth = tempSpan.offsetWidth; | |
document.body.removeChild(tempSpan); | |
// テキストだけの幅とパディング、ボーダーを加算して幅を最終計算する | |
const paddingLeft = parseFloat(computedStyle.paddingLeft); | |
const paddingRight = parseFloat(computedStyle.paddingRight); | |
const borderLeftWidth = parseFloat(computedStyle.borderLeftWidth); | |
const borderRightWidth = parseFloat(computedStyle.borderRightWidth); | |
const totalWidth = | |
textWidth + paddingLeft + paddingRight + borderLeftWidth + borderRightWidth; | |
widthRef.current = totalWidth; | |
} | |
}, [selectRef]); | |
// 初期レンダリング時と値が変更された時に幅を計算する | |
useLayoutEffect(() => { | |
calculateWidth(); | |
}, [calculateWidth, currentValue]); | |
// リサイズ時にも幅を再計算する | |
useLayoutEffect(() => { | |
const handleResize = () => { | |
calculateWidth(); | |
}; | |
window.addEventListener('resize', handleResize); | |
return () => { | |
window.removeEventListener('resize', handleResize); | |
}; | |
}, [calculateWidth]); | |
return selectWidth; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment