From 680b30a04541caa087256dea6122183c71581bc4 Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Tue, 20 Jan 2026 08:32:42 -0700 Subject: [PATCH] fix: added cursor rules for use static event effect (#41993) fix: added use static event rule --- .cursor/rules/studio-useStaticEffectEvent.mdc | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 .cursor/rules/studio-useStaticEffectEvent.mdc diff --git a/.cursor/rules/studio-useStaticEffectEvent.mdc b/.cursor/rules/studio-useStaticEffectEvent.mdc new file mode 100644 index 00000000000..07e82b5e4fa --- /dev/null +++ b/.cursor/rules/studio-useStaticEffectEvent.mdc @@ -0,0 +1,161 @@ +--- +description: Guidelines for using the useStaticEffectEvent hook in Studio - a polyfill for React's useEffectEvent pattern +alwaysApply: false +--- + +# useStaticEffectEvent Hook + +The `useStaticEffectEvent` hook (located at `apps/studio/hooks/useStaticEffectEvent.ts`) is a userland implementation of React's `useEffectEvent` pattern. It solves the stale closure problem by providing a stable callback reference that always accesses the latest props and state values. + +## What Problem Does It Solve? + +When using `useEffect`, you often need to access props or state inside your Effect, but you don't want changes to those values to re-run the Effect. Without `useStaticEffectEvent`, you'd face two bad options: + +1. **Add them to dependencies** - causes unnecessary Effect re-runs (teardown/reconnect cycles) +2. **Omit from dependencies** - causes stale closure bugs where your callback uses outdated values + +```tsx +// Problem: This Effect 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 re-runs + }) + return () => connection.disconnect() +}, [roomId, theme]) // Adding theme causes unnecessary reconnections +``` + +## When to Use useStaticEffectEvent + +Use `useStaticEffectEvent` when you need to: + +1. **Read latest state/props inside an Effect without re-triggering it** +2. **Create stable callbacks that always use current values** +3. **Avoid stale closure bugs in event handlers used within Effects** + +### Pattern 1: Syncing data without re-running on every change + +```tsx +// ✅ Good - sync data when status changes, but always read latest state +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]) +``` + +### Pattern 2: Stable callbacks for async operations + +```tsx +// ✅ Good - wrap complex async logic that reads many values +const exportInternal = useStaticEffectEvent( + async ({ bypassConfirmation }: { bypassConfirmation: boolean }): Promise => { + if (!params.enabled) return + const { projectRef, connectionString, entity, totalRows } = params + // ... complex async logic using latest params + } +) + +// This callback is stable and can be safely used in useCallback +const exportInDesiredFormat = useCallback( + () => exportInternal({ bypassConfirmation: false }), + [exportInternal] +) +``` + +### Pattern 3: Infinite scroll / pagination triggers + +```tsx +// ✅ Good - always read latest pagination state when scrolling triggers fetch +const fetchNext = useStaticEffectEvent(() => { + if (lastItem && lastItem.index >= items.length - 1 && hasNextPage && !isFetchingNextPage) { + fetchNextPage() + } +}) + +useEffect(fetchNext, [lastItem, fetchNext]) +``` + +## When NOT to Use useStaticEffectEvent + +### Don't use it to avoid specifying legitimate dependencies + +```tsx +// ❌ Bad - hiding the fact that this should re-run when roomId changes +const connect = useStaticEffectEvent(() => { + const connection = createConnection(roomId) + connection.connect() +}) + +useEffect(() => { + connect() // BUG: Won't reconnect when roomId changes! +}, [connect]) + +// ✅ Good - roomId is a legitimate dependency +useEffect(() => { + const connection = createConnection(roomId) + connection.connect() + return () => connection.disconnect() +}, [roomId]) +``` + +### Don't use it for simple event handlers outside Effects + +```tsx +// ❌ Unnecessary - not used inside an Effect +const handleClick = useStaticEffectEvent(() => { + console.log(count) +}) + +// ✅ Good - regular function or useCallback is fine +const handleClick = () => { + console.log(count) +} +``` + +## How It Works + +The hook uses a ref to store the latest callback and returns a stable wrapper function: + +```tsx +export const useStaticEffectEvent = (callback: Callback) => { + const callbackRef = useRef(callback) + + // Update the ref on every render with the latest callback + useLayoutEffect(() => { + callbackRef.current = callback + }) + + // Return a stable function that calls the latest callback + const eventFn = useCallback((...args: any) => { + return callbackRef.current(...args) + }, []) + + return eventFn as unknown as Callback +} +``` + +## Relationship to React's useEffectEvent + +This hook is a polyfill for React's experimental `useEffectEvent` (now stable in React 19.2). The core concept is identical: + +- Extract non-reactive logic into a stable function +- Always access the latest props/state without adding them as Effect dependencies +- Should only be called from within Effects + +When React's `useEffectEvent` becomes widely available, this hook can be replaced with the official API. + +## Rules + +1. **Only call the returned function inside Effects** (useEffect, useLayoutEffect) +2. **Don't pass the function to other components or hooks** as a callback prop +3. **Use for non-reactive logic only** - logic that reads values but shouldn't trigger re-runs +4. **Include it in dependency arrays** when used in useEffect (the function is stable, so it won't cause re-runs)