Skip to content

refactor(optimization): Replace useOptimization with useOptimizationC…#352

Closed
Lotfi Anwar L Arif (Lotfi-Arif) wants to merge 1 commit into
mainfrom
NT-3560-optimization-provider-returns-null-on-ssr-blank-page-in-next-js-app-router-and-other-ssr-environments
Closed

refactor(optimization): Replace useOptimization with useOptimizationC…#352
Lotfi Anwar L Arif (Lotfi-Arif) wants to merge 1 commit into
mainfrom
NT-3560-optimization-provider-returns-null-on-ssr-blank-page-in-next-js-app-router-and-other-ssr-environments

Conversation

@Lotfi-Arif

@Lotfi-Arif Lotfi Anwar L Arif (Lotfi-Arif) commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Problem

OptimizationProvider returned null when isReady was false. On the server, useLayoutEffect never fires, so isReady never becomes true, and the provider always returned null — blank page on every SSR render.

Root cause

The original gate assumed "not ready = nothing to show." On the server that assumption is always true, so it silently stripped the entire subtree from HTML output.

The Fix

Flipped the question. Instead of "detect SSR and skip the gate," we asked "when should the gate actually fire?" — only on the client, after the provider has mounted.
A single hasMountedRef (flipped inside useLayoutEffect) tracks this. Gate condition becomes hasMountedRef.current && !isReady. On the server the ref stays false, children always render. On the client the gate fires normally until the SDK is ready. No SSR detection needed.

Why previous attempts failed

Every attempt tried to detect "server vs client" at render time — typeof window, useId tricks, isPreHydration refs — but React gives no clean signal at render time. The server render and the initial synchronous client render are indistinguishable. Every detection heuristic either broke jsdom, leaked state, or introduced extra re-renders.

Downstream surface area

With children now rendering on the server, SDK hooks called at render time would throw (sdk is undefined). Fixed the SDK's own internal hooks — useOptimizationActions, useOptimizationState (5 hooks), useEntryResolver, useMergeTagResolver, useAutoPageEmitter, NextjsOptimizationState — to read from useOptimizationContext() and safely default when sdk is undefined. useOptimization() public contract is unchanged.
Consumer code in nextjs-sdk_ssr and nextjs-sdk_hybrid that called useOptimization() at render time was updated to use useOptimizationContext() with a sdk guard — same pattern the SDK now uses internally.

What we chose not to do

  • Considered a parallel Next.js SDK path with server/client switching — rejected because it duplicates 20+ hooks/components, creates two mental models for consumers, and the switch logic has the same "detect SSR" problem at its core.
  • Considered changing useOptimization() to return undefined instead of throwing — deferred; it's a public API break that can be revisited separately.

Validation

  • react-web-sdk: typecheck clean, 104/104 unit tests passing, bundle within budget.
  • nextjs-sdk: typecheck clean, 27/27 unit tests passing, bundle within budget.
  • nextjs-sdk_ssr and nextjs-sdk_hybrid: typecheck clean after reinstall.
  • pnpm lint and pnpm format:check clean across workspace.

…ontext across components and hooks

- Updated ControlPanel and CustomViewTracker components to utilize useOptimizationContext for SDK access.
- Refactored hooks to ensure safe handling of SDK state, preventing errors when SDK is not ready.
- Enhanced SSR compatibility by ensuring components and hooks return appropriate defaults when SDK is unavailable.
- Improved overall code consistency and maintainability by standardizing SDK access patterns.

This change streamlines the integration of the optimization SDK and enhances the robustness of the application during server-side rendering.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant