use-static-effect-event について
useStaticEffectEvent は React の useEffectEvent のユーザーランド実装で、Effect を再実行せずに最新の props と state を読み取る安定したコールバックを提供します。Effect 内の非リアクティブな処理向けで、古いクロージャの問題を避けます。
主な機能
- 01最新の state と props を読む安定したコールバック
- 02古いクロージャの回避
- 03不要な Effect の再実行を防止
- 04非同期処理やページネーションのトリガーに対応
活用シーン
- 01Effect を再実行せず最新値を読み取る
- 02非同期処理用の安定したコールバックを作成
- 03無限スクロールで次ページの読み込みを開始
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ supabase/ supabase --skill use-static-effect-eventインストール後、Claude Code で/use-static-effect-eventを入力して読み込まれたか確認します。
詳細
useStaticEffectEvent
apps/studio/hooks/useStaticEffectEvent.ts にあります。
React の useEffectEvent(React 19.2 で安定版)をユーザーランドでポリフィルしたものです。古いクロージャの問題を解決します。値の変化によって Effect が再実行されることなく、最新の props/state を常に読み取る安定したコールバックを提供します。
解決する問題
これを使わない場合、useEffect 内では次のどちらかを選ぶ必要があり、どちらにも問題があります。
- 値を依存配列に追加する → 不要な Effect の再実行(クリーンアップ/再接続)
- 依存配列から省く → 古いクロージャによるバグ(値が更新されない)
// Problem: re-runs every time `theme` changes, even though we only
// want to reconnect when `roomId` changes
useEffect(() => {
const connection = createConnection(roomId)
connection.on('connected', () => {
showNotification('Connected!', theme) // theme causes unwanted reconnects
})
return () => connection.disconnect()
}, [roomId, theme])
使用する場面
- Effect を再実行せずに、Effect 内で最新の state/props を読み取る
- 現在の値を常に使う安定したコールバックを作成する
- Effect 内で使用するイベントハンドラーで、古いクロージャによる問題を回避する
パターン 1:変更のたびに再実行せずにデータを同期する
const syncApiPrivileges = useStaticEffectEvent(() => {
if (hasLoadedInitialData.current) return
if (!apiAccessStatus.isSuccess) return
if (!privilegesForTable) return
hasLoadedInitialData.current = true
setPrivileges(privilegesForTable.privileges)
})
useEffect(() => {
syncApiPrivileges()
}, [apiAccessStatus.status, syncApiPrivileges])
パターン 2:非同期処理用の安定したコールバック
const exportInternal = useStaticEffectEvent(
async ({ bypassConfirmation }: { bypassConfirmation: boolean }) => {
if (!params.enabled) return
const { projectRef, connectionString, entity, totalRows } = params
// complex async logic using latest params
}
)
// Stable reference — safe to use in useCallback
const exportInDesiredFormat = useCallback(
() => exportInternal({ bypassConfirmation: false }),
[exportInternal]
)
パターン 3:無限スクロール/ページネーションのトリガー
const fetchNext = useStaticEffectEvent(() => {
if (lastItem && lastItem.index >= items.length - 1 && hasNextPage && !isFetchingNextPage) {
fetchNextPage()
}
})
useEffect(fetchNext, [lastItem, fetchNext])
使用しない場面
正当な依存関係を隠すために使わないでください。
// ❌ Bad — roomId IS a legitimate dependency; this hides a bug
const connect = useStaticEffectEvent(() => {
const connection = createConnection(roomId)
connection.connect()
})
useEffect(() => {
connect()
}, [connect]) // Won't reconnect when roomId changes!
// ✅ roomId belongs in deps
useEffect(() => {
const connection = createConnection(roomId)
connection.connect()
return () => connection.disconnect()
}, [roomId])
Effect の外で使う単純なイベントハンドラーには使用しないでください。
// ❌ Unnecessary — not used inside an Effect
const handleClick = useStaticEffectEvent(() => console.log(count))
// ✅ Regular function is fine
const handleClick = () => console.log(count)
ルール
- 返された関数は Effect 内(
useEffect、useLayoutEffect)でのみ呼び出してください。 - コールバックプロパティとして、ほかのコンポーネントやフックに渡さないでください。
- 非リアクティブなロジックにのみ使用してください。値を読み取りますが、再実行のトリガーにはなりません。
useEffectで使用する場合は、依存配列に含めてください(安定しているため、再実行は発生しません)。
仕組み
export const useStaticEffectEvent = <Callback extends Function>(callback: Callback) => {
const callbackRef = useRef(callback)
useLayoutEffect(() => {
callbackRef.current = callback // always latest
})
const eventFn = useCallback((...args: any) => {
return callbackRef.current(...args)
}, []) // stable reference
return eventFn as unknown as Callback
}
コメント
Markdown 対応