Skip to content

React Compiler Compatibility ​

@crimson_dev/use-resize-observer is verified compatible with the React Compiler (formerly React Forget). You can enable the compiler without any changes to code that uses this hook.

Why It Matters ​

The React Compiler automatically memoizes component renders, eliminating the need for manual useMemo, useCallback, and React.memo. However, hooks that violate React's rules or create unstable references can break under compilation, producing stale closures or infinite re-renders.

This library is designed from the ground up to work correctly with the compiler.

How We Ensure Compatibility ​

1. Stable Callback Identity ​

The onResize callback uses a ref-based pattern equivalent to useEffectEvent:

typescript
// Internal implementation (simplified)
const onResizeRef = useRef(onResize);
onResizeRef.current = onResize; // Always latest closure

// Used inside useEffect — stable identity, always-current closure
const stableCallback = (entry: ResizeObserverEntry) => {
  onResizeRef.current?.(entry);
};

The compiler correctly identifies onResizeRef as a ref and exempts it from memoization analysis. The callback identity never changes, so the effect never re-runs due to callback changes.

2. No Conditional Hooks ​

All hooks are called unconditionally at the top level of the custom hook. The pool and scheduler are initialized lazily inside useEffect, not behind conditionals:

typescript
// Correct: unconditional hook calls (simplified — see src/hook.ts for the real implementation)
export function useResizeObserver<E extends Element>(options?: Options<E>) {
  const [size, setSize] = useState<Size>({ width: undefined, height: undefined });
  const internalRef = useRef<E>(null);
  const ref = options?.ref ?? internalRef;

  useEffect(() => {
    // Lazy initialization happens here, not at the hook level
    const element = ref.current;
    if (!element) return;
    // ...
  }, [options?.box]);

  return { ref, ...size };
}

3. No Mutable State Leaks ​

All internal state uses useState (not external mutable stores, module-level variables, or useRef for render-visible state). The compiler can safely track all state transitions.

4. Pure Return Values ​

The hook's return value ({ ref, width, height }) is derived entirely from React state and refs. No external mutable data is exposed through the return value.

Verification ​

A dedicated compiler Vitest project runs the hook test suite with babel-plugin-react-compiler applied, so every assertion executes against compiler-transformed components. It is part of the default npm run test run:

bash
# Run the React Compiler compatibility project on its own
npx vitest run --project compiler

# Or run all projects (unit + compiler + browser)
npm run test

Verified Scenarios ​

ScenarioStatus
Basic useResizeObserver with default optionsVerified
External ref forwardingVerified
onResize callback with changing closuresVerified
onResize callback that captures component stateVerified
useResizeObserverEntries with dynamic ref arraysVerified
createResizeObserver factory (non-hook, no compiler impact)Verified
Hot module replacement during developmentVerified
Component wrapped in React.memo (redundant but safe)Verified
Component with sibling useMemo/useCallback callsVerified

What NOT to Do ​

Do not wrap onResize in useCallback

The hook already stabilizes the callback identity internally. Adding useCallback creates unnecessary memoization overhead that the compiler will also try to optimize, potentially leading to confusion:

tsx
// BAD — unnecessary double-memoization
const onResize = useCallback((entry: ResizeObserverEntry) => {
  setSize(entry.contentRect);
}, []);

const { ref } = useResizeObserver({ onResize });

// GOOD — just pass the function directly
const { ref } = useResizeObserver({
  onResize: (entry) => {
    setSize(entry.contentRect);
  },
});

Do not destructure in the dependency array

If you are manually writing useEffect that depends on the hook's return values, do not try to be clever with destructuring in the dependency array. Just use the values directly:

tsx
const { width, height } = useResizeObserver<HTMLDivElement>();

// GOOD — straightforward
useEffect(() => {
  if (width !== undefined) {
    updateLayout(width, height!);
  }
}, [width, height]);

// The compiler handles this automatically — you don't need the manual useEffect at all

Compiler Escape Hatches ​

If you need to opt a specific component out of compiler optimization (rare), use the 'use no memo' directive:

tsx
function SpecialComponent() {
  'use no memo';

  const { ref, width } = useResizeObserver<HTMLDivElement>();
  // This component will not be auto-memoized
  return <div ref={ref}>{width}</div>;
}

This should almost never be necessary with this library.

Testing Compiler Compatibility in Your App ​

To verify that the compiler works correctly with your usage of useResizeObserver:

  1. Enable the React Compiler in your build:
typescript
// babel.config.ts
module.exports = {
  plugins: [
    ['babel-plugin-react-compiler', { target: '19' }],
  ],
};
  1. Run your test suite and watch for:

    • Stale dimension values after resize
    • Callbacks not firing after resize
    • Infinite re-render loops
    • Missing ref assignments
  2. If any issues appear, they are likely in your component code, not in the hook. Check for violations of the Rules of React.

Next Steps ​

  • Signals -- Signal integration patterns (also compiler-safe)
  • Performance -- How compiler optimization affects resize performance
  • Troubleshooting -- Common issues with compiler + resize observer

Released under the MIT License.