Skip to content

MSD: Refactor interim omnibar to hooks-based data flow#109881

Merged
p-jackson merged 12 commits intotrunkfrom
refactor/interim-omnibar-hooks-data-flow
Apr 9, 2026
Merged

MSD: Refactor interim omnibar to hooks-based data flow#109881
p-jackson merged 12 commits intotrunkfrom
refactor/interim-omnibar-hooks-data-flow

Conversation

@StevenDufresne
Copy link
Copy Markdown
Contributor

@StevenDufresne StevenDufresne commented Apr 8, 2026

Proposed Changes

  • Refactor loadOmnibar to mount a hooks-based container (useInterimOmnibarData) instead of driving React imperatively with root.render and QueryObserver.subscribe after hydrateRoot.

Why are these changes being made?

The previous implementation raced hydration and caused React to bail with:

This root received an early update, before anything was able to hydrate. Switched the entire root to client rendering.

With the refactor, every post-hydration update flows through normal React rendering — there is no external code path that can update the root between hydrateRoot and its commit.

Testing Instructions

  1. Enable the dashboard/omnibar feature flag.
  2. Hard-reload a Dashboard page with the omnibar visible.
  3. Confirm the "This root received an early update…" warning no longer appears in the console.
  4. Confirm the omnibar still shows the current user and the active site, and re-renders when switching sites.
  5. Confirm the mobile menu and notifications toggles still work.

Pre-merge Checklist

  • Has the general commit checklist been followed? (PCYsg-hS-p2)
  • Have you written new tests for your changes?
  • Have you tested the feature in Simple (P9HQHe-k8-p2), Atomic (P9HQHe-jW-p2), and self-hosted Jetpack sites (PCYsg-g6b-p2)?
  • Have you checked for TypeScript, React or other console errors?
  • Have you tested accessibility for your changes? Ensure the feature remains usable with various user agents (e.g., browsers), interfaces (e.g., keyboard navigation), and assistive technologies (e.g., screen readers) (PCYsg-S3g-p2).
  • Have you used memoizing on expensive computations?
  • Have we added the "[Status] String Freeze" label as soon as any new strings were ready for translation (p4TIVU-5Jq-p2)?
  • For changes affecting Jetpack: Have we added the "[Status] Needs Privacy Updates" label if this pull request changes what data or activity we track or use (p4TIVU-aUh-p2)?

@StevenDufresne StevenDufresne marked this pull request as ready for review April 8, 2026 05:55
@StevenDufresne StevenDufresne requested a review from a team as a code owner April 8, 2026 05:55
@matticbot matticbot added the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Apr 8, 2026
Copy link
Copy Markdown
Contributor

@fushar fushar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than that I believe this is good 👍 Tested.

@StevenDufresne StevenDufresne force-pushed the refactor/interim-omnibar-hooks-data-flow branch from 97c98cf to b03137c Compare April 9, 2026 02:44
Copy link
Copy Markdown
Member

@p-jackson p-jackson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this might produce errors because the <InterimOmnibar> rendered on the server at client/document/index.jsx doesn't have the container and the client-side one does. I thought that's the sort of thing that breaks hydration.
Evidently not.

A nit is that I would perhaps combine the container and hook into a single tsx file, since they really only work together.


const { data: site = null } = useQuery( {
...siteByIdQuery( siteId ?? 0 ),
enabled: hydrated && !! siteId,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to add a isRecentSitesLoaded check here too. Otherwise the omnibar could flash the primary blog details before switching to the recent site details.

The problem is that the siteByIdQuery is going to start up as soon as there is any site ID available, but we really want it to wait for the preferences to finish loading first.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was meaning to fix this behavior in my next PR 😄 it's related to the origin_site_id check, where it trumps everything.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah definitely related to that!

Comment on lines +62 to +63
currentRoute: window.location.pathname,
};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should return the callbacks in this case too. It's a small thing, but the <InterimOmnibar> rebuilds the redux store whenever this callback changes, so we don't need these callbacks to unnecessarily change.

Suggested change
currentRoute: window.location.pathname,
};
currentRoute: window.location.pathname,
onToggleMenu,
onToggleNotifications,
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which means we should fix up the hook's jsdoc too.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand, why didn't we also do this (returning callbacks pre-hydration) before this PR?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the .render() call was always passing the callback props. The hydrateRoot call wasn't, but that was my mistake. I suspect that between hydrateRoot and .render is was probably tearing down all the elements any, and so it was re-creating a store then no matter what.

@p-jackson p-jackson force-pushed the refactor/interim-omnibar-hooks-data-flow branch from b03137c to bf4d95a Compare April 9, 2026 03:26
StevenDufresne and others added 11 commits April 9, 2026 15:26
The previous implementation drove React imperatively via `root.render`
and `QueryObserver.subscribe` after `hydrateRoot`, which raced hydration
and caused React to bail out with "This root received an early update,
before anything was able to hydrate. Switched the entire root to client
rendering."

Move the data fetching into a new `InterimOmnibarContainer` that uses
TanStack Query hooks (`useQuery` for auth, recentSites, and the active
site). A `useState(false)` + `useEffect` gate ensures the first render
mirrors the SSR output exactly (`user = initialUser`, `site = null`, no
toggle callbacks); after hydration commits, the container switches to
hook-driven data and wires up the toggle handlers. All post-hydration
updates now flow through normal React rendering instead of external
imperative calls.

`loadOmnibar` shrinks to just click delegation and a single
`hydrateRoot` call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract the data-flow logic into an exported `useInterimOmnibarData`
hook so it can be tested in isolation without rendering the full
masterbar tree. Add unit tests covering: bootstrapped user via
initialData, toggle callback wiring, recent-site loading, primary_blog
fallback, and the null-site state when no siteId is known.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the weak assertions (`initialData` pass-through and the
`site === null` default) with a `renderToStaticMarkup`-based test that
verifies the hook's pre-hydration output matches the SSR shape exactly
(user, null site, no callbacks). This is the contract hydration
actually depends on.

Drop the "keeps site null" test — it only asserted a destructuring
default and would have passed even if the enabled guard was removed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move the hook out of `interim-omnibar-container.tsx` into
`use-interim-omnibar-data.ts`, matching the source-file naming
convention used by `hooks/use-persistent-view.ts` (and lining up with
the test file name).

Tests now build a fresh `OmnibarEvents` per test instead of subscribing
to the shared module singleton, so a failed assertion can't leak a
dangling subscription into later tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Avoids nesting a second provider inside InterimOmnibar's own.
@p-jackson p-jackson force-pushed the refactor/interim-omnibar-hooks-data-flow branch from bf4d95a to 9cf950d Compare April 9, 2026 03:27
Copy link
Copy Markdown
Member

@p-jackson p-jackson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tested it with wpcom-bootstrap-user too to make sure there's no errors still.

Finally no race conditions in our hydration I think 🤞🤞🤞🤞

Copy link
Copy Markdown
Contributor

@fushar fushar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

const { data: site = null } = useQuery(
{
...siteByIdQuery( siteId ?? 0 ),
enabled: hydrated && !! siteId && ! isRecentSitesLoading,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems this means the queries can't run in parallel. In any case, I'll try to fix it up later.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes Claude was mad at me for breaking the parallel requests. But I think following it up once we have the final ordering of site IDs makes sense.

@p-jackson p-jackson merged commit 9c43891 into trunk Apr 9, 2026
15 checks passed
@p-jackson p-jackson deleted the refactor/interim-omnibar-hooks-data-flow branch April 9, 2026 03:47
@github-actions github-actions bot removed the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Apr 9, 2026
@fushar
Copy link
Copy Markdown
Contributor

fushar commented Apr 9, 2026

🎉

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.

4 participants