Skip to content

SSR & React Server Components ​

@crimson_dev/use-resize-observer is safe for SSR and RSC environments out of the box.

The Problem ​

ResizeObserver is a browser API -- it does not exist in Node.js or edge runtimes. Naively importing a ResizeObserver hook in an SSR context causes ReferenceError: ResizeObserver is not defined.

Solution: 'use client' Boundary ​

The main hook entry includes 'use client' at the top. This tells React Server Components that this module must run on the client:

tsx
// This file has 'use client' — safe to import in RSC trees
import { useResizeObserver } from '@crimson_dev/use-resize-observer';

React will automatically create a client boundary at the import point. No additional configuration is needed.

Server Component imports

If you import the hook directly inside a Server Component (without a client component boundary), React will throw a clear error about violating the 'use client' boundary. You will not see a cryptic hook error.

Next.js App Router Integration ​

The recommended pattern with Next.js App Router:

tsx
// app/components/ResizablePanel.tsx
'use client';

import { useResizeObserver } from '@crimson_dev/use-resize-observer';

export const ResizablePanel = () => {
  const { ref, width, height } = useResizeObserver<HTMLDivElement>();

  return (
    <div ref={ref} className="panel">
      {width !== undefined ? `${width}x${height}` : 'Measuring...'}
    </div>
  );
};
tsx
// app/page.tsx — Server Component (default in App Router)
import { ResizablePanel } from './components/ResizablePanel';

export default function Page() {
  return (
    <main>
      <h1>Dashboard</h1>
      <ResizablePanel /> {/* Client component boundary */}
    </main>
  );
}

Server Mock ​

For SSR frameworks that pre-render component trees on the server, use the /server entry to get a type-safe mock:

tsx
import { createServerResizeObserverMock } from '@crimson_dev/use-resize-observer/server';

// Returns { ref: { current: null }, width: undefined, height: undefined, entry: undefined }
const mockResult = createServerResizeObserverMock<HTMLDivElement>();

The mock returns undefined for all dimensions, matching the hook's initial state before the first measurement. This ensures your components render consistently between server and client on the initial pass.

Avoiding layout shift

You can spread the mock result and override dimensions to approximate the expected client size, reducing Cumulative Layout Shift (CLS):

tsx
const result = { ...createServerResizeObserverMock<HTMLDivElement>(), width: 1024, height: 768 };

Environment Detection ​

tsx
import { isResizeObserverSupported } from '@crimson_dev/use-resize-observer/server';

if (isResizeObserverSupported()) {
  // Browser — safe to use the real hook
} else {
  // Server — use mock or conditionally render
}

Remix / React Router Integration ​

tsx
// app/components/ResponsiveChart.tsx
'use client';

import { useResizeObserver } from '@crimson_dev/use-resize-observer';

export function ResponsiveChart() {
  const { ref, width, height } = useResizeObserver<HTMLDivElement>();

  return (
    <div ref={ref} style={{ width: '100%', minHeight: 300 }}>
      {width !== undefined && height !== undefined ? (
        <svg viewBox={`0 0 ${width} ${height}`}>
          {/* Chart content */}
        </svg>
      ) : (
        <div style={{ height: 300 }}>Loading chart...</div>
      )}
    </div>
  );
}

Astro Integration ​

With Astro's island architecture, the hook works inside any React island:

astro
---
// src/pages/index.astro
import ResizableWidget from '../components/ResizableWidget.tsx';
---

<html>
  <body>
    <ResizableWidget client:visible />
  </body>
</html>
tsx
// src/components/ResizableWidget.tsx
import { useResizeObserver } from '@crimson_dev/use-resize-observer';

export default function ResizableWidget() {
  const { ref, width } = useResizeObserver<HTMLDivElement>();
  return <div ref={ref}>{width ?? 'Hydrating...'}px wide</div>;
}

ResizeObserverContext for Testing ​

Use ResizeObserverContext to inject a mock ResizeObserver in test environments:

tsx
import { ResizeObserverContext } from '@crimson_dev/use-resize-observer';

const MockResizeObserver = class {
  observe() {}
  unobserve() {}
  disconnect() {}
} as unknown as typeof ResizeObserver;

const TestWrapper = ({ children }: { children: React.ReactNode }) => (
  <ResizeObserverContext value={MockResizeObserver}>
    {children}
  </ResizeObserverContext>
);

// In your test
import { render } from '@testing-library/react';

test('renders without ResizeObserver', () => {
  const { getByText } = render(
    <TestWrapper>
      <MyComponent />
    </TestWrapper>
  );
  expect(getByText('Measuring...')).toBeDefined();
});

Vitest / Jest Configuration ​

Most test runners do not provide ResizeObserver globally. You have two options:

Option 1: Global mock ​

typescript
// vitest.setup.ts
class ResizeObserverMock {
  observe() {}
  unobserve() {}
  disconnect() {}
}
globalThis.ResizeObserver = ResizeObserverMock as unknown as typeof ResizeObserver;

Use ResizeObserverContext as shown above. This is more explicit and does not pollute the global scope.

Worker Mode and SSR ​

The worker hook (useResizeObserverWorker) is browser-only. The /worker entry includes 'use client' and should only be used in client components:

tsx
'use client';

import { useResizeObserverWorker } from '@crimson_dev/use-resize-observer/worker';

// Safe: this module is client-only via 'use client'
const MyWorkerComponent = () => {
  const { ref, width, height } = useResizeObserverWorker<HTMLDivElement>();
  return <div ref={ref}>{width} x {height}</div>;
};

Next Steps ​

Released under the MIT License.