Skip to content

fix: bridge SSR payload mismatch when client search provider differs from server default#2379

Open
gusa4grr wants to merge 1 commit intonpmx-dev:mainfrom
gusa4grr:search-ssr-mismatch-bridge
Open

fix: bridge SSR payload mismatch when client search provider differs from server default#2379
gusa4grr wants to merge 1 commit intonpmx-dev:mainfrom
gusa4grr:search-ssr-mismatch-bridge

Conversation

@gusa4grr
Copy link
Copy Markdown
Contributor

@gusa4grr gusa4grr commented Apr 4, 2026

🔗 Linked issue

Resolves #2380

🧭 Context

When the user's searchProvider value is npm, there will be hydration errors and more. Routes affected are search?q=vue or /settings page, but it may be more, like /~[username] route

Moreover, on search?q=vue, the user is additionally presented with "no results screen" after flashing actual search results 😅

In this MR I:

  • I introduced the Search SSR Bridge between both searchProviders, as I found that the results are of the same structure and assumed I could "reuse" them during hydration
  • added vue org to the e2e fixture, so it mimics now the same scenario as on the prod app, as before in tests, there were no "org" checks in search results
  • aligned the "interaction" spec with arrow-up/down behaviour to accommodate new expected behaviour

In order to see this behaviour yourself, you need to:

  1. Go to settings, change your provider to npm from algolia
  2. write in url bar manually /search?q=vue. Observe hydration + no results
  3. write in url bar manually /settings. Observe hydration error

More details in "details" section below 😉

Images attached:

search page:

image

settings page:

image
  • Centralize ?p query param merge into useSearchProvider() getter
  • Remove duplicated searchProviderValue computed from all consumers
  • Add bridgeSearchSSRPayload() to copy SSR cache to client's provider key during hydration
  • Destructure asyncData before spreading to avoid shadowing custom data ref
  • Update e2e tests for org suggestion cards in keyboard navigation

📚 Description

During my adventures with the implementing server-synced user preferences (please check that one too if you fancy 🙂 ), I stumbled upon the e2e tests failing with hydration errors, which were:

  • landing on /search?q=vue
  • settings page
    Also, the interactions spec and url-compatibility spec have each failed tests 🤯

So I went ahead and started digging into what "have I done" so it broke.
after bunch of hours of little to no success I (finally) decided to check how does the main branch behave and just went ahead to npmx.dev prod app and I found that with npm as searchProvider, th things are actually not working as they should.

This became visible during my implementation, as preferences are now server-synced, which just uncovered those issues. And the combination of the fact that in the "test" environment, searchProvideris defaulted tonpm`.

I continued digging and found a misalignment in the searchProvider value during hydration. Basically the fetch was always happening with algolia, and then when NPM "results" were tried to be hydrated on client - it sees nothing due to cache key mismatch between providers.

so I dig more and more and I come up with the solution you see in this MR.

I'm happy to discuss further or implement a better approach.

NOTE: When navigating via the website to the search page, all is OK. like for instance if you're on the /settings and write in npmx search bar vue => it will render OK. The secret here is the ?p=npm query parameter, which is programmatically added, thereby enabling the search page to find results.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Apr 4, 2026 11:16pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Apr 4, 2026 11:16pm
npmx-lunaria Ignored Ignored Apr 4, 2026 11:16pm

Request Review

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 4, 2026

Codecov Report

❌ Patch coverage is 56.52174% with 10 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/composables/useGlobalSearch.ts 0.00% 5 Missing ⚠️
app/composables/npm/search-utils.ts 60.00% 3 Missing and 1 partial ⚠️
app/composables/npm/useSearch.ts 66.66% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@gusa4grr
Copy link
Copy Markdown
Contributor Author

gusa4grr commented Apr 4, 2026

Fixes #2380

@gusa4grr gusa4grr force-pushed the search-ssr-mismatch-bridge branch from d5c343d to 2d26409 Compare April 4, 2026 23:13
- Centralize `?p` query param merge into `useSearchProvider()`
- Remove duplicated `searchProviderValue` computed from all consumers
- Add `bridgeSearchSSRPayload()` to copy SSR cache to client's provider key during hydration
- Destructure asyncData before spreading to avoid shadowing custom `data` ref
- Update e2e tests for org suggestion cards in keyboard navigation
@gusa4grr gusa4grr force-pushed the search-ssr-mismatch-bridge branch from 2d26409 to 180d757 Compare April 4, 2026 23:14
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

📝 Walkthrough

Walkthrough

This pull request removes route-query-based provider normalization and switches composables/components to use the reactive searchProvider directly. It adds bridgeSearchSSRPayload(prefix, identifier, provider) to copy Algolia SSR payloads into provider-specific client cache keys during client hydration when appropriate, avoiding refetches. Several npm composables (useOrgPackages, useSearch, useUserPackages, useGlobalSearch) and the SearchProviderToggle component were updated to use searchProvider and to adjust cache keys; e2e keyboard-navigation tests were updated for org suggestion cards and a new npm fixture for @vue packages was added.

Possibly related issues

Possibly related PRs

Suggested labels

needs review

Suggested reviewers

  • alexdln
  • danielroe
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description is comprehensive and clearly related to the changeset, explaining hydration errors when searchProvider is npm and detailing the implemented solution.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
test/e2e/interactions.spec.ts (1)

105-119: Consider adding explicit focus assertion for consistency.

The other modified tests (lines 143, 170, 177) assert that orgSuggestion is focused after ArrowDown, but this test relies solely on the final URL check. Adding an assertion after line 117 would make the test more consistent and easier to debug if it fails.

🔧 Suggested improvement
     // ArrowDown again, then Enter navigates to the suggestion
     // URL is /package/vue or /org/vue or /user/vue. Not /vue
     await page.keyboard.press('ArrowDown')
+    await expect(orgSuggestion).toBeFocused()
     await page.keyboard.press('Enter')
     await expect(page).toHaveURL(/\/(package|org|user)\/vue/)

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: daf4de20-08e0-4555-b419-178494a8c1a0

📥 Commits

Reviewing files that changed from the base of the PR and between 5fe1486 and d5c343d.

📒 Files selected for processing (9)
  • app/components/SearchProviderToggle.client.vue
  • app/composables/npm/search-utils.ts
  • app/composables/npm/useOrgPackages.ts
  • app/composables/npm/useSearch.ts
  • app/composables/npm/useUserPackages.ts
  • app/composables/useGlobalSearch.ts
  • app/composables/useSettings.ts
  • test/e2e/interactions.spec.ts
  • test/fixtures/npm-registry/search/@vue.json

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
app/composables/npm/search-utils.ts (1)

1-26: Well-implemented SSR payload bridge.

The logic correctly handles the hydration mismatch by copying SSR-cached data from the algolia-keyed entry to the client's provider-specific key. The guard conditions are comprehensive:

  • import.meta.client ensures client-only execution
  • isHydrating ensures this only runs during hydration
  • id && prevents malformed keys from empty identifiers
  • The !nuxtApp.payload.data[clientKey] check avoids overwriting existing data

Consider using the stricter SearchProvider type instead of string for the provider parameter:

♻️ Optional: Stricter typing
+import type { SearchProvider } from '~/composables/useSettings'
+
 export function bridgeSearchSSRPayload(
   prefix: string,
   identifier: MaybeRefOrGetter<string>,
-  provider: MaybeRefOrGetter<string>,
+  provider: MaybeRefOrGetter<SearchProvider>,
 ): void {

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b0bd3e3f-7b8b-48bb-b5b8-db13fd315b81

📥 Commits

Reviewing files that changed from the base of the PR and between d5c343d and 180d757.

📒 Files selected for processing (9)
  • app/components/SearchProviderToggle.client.vue
  • app/composables/npm/search-utils.ts
  • app/composables/npm/useOrgPackages.ts
  • app/composables/npm/useSearch.ts
  • app/composables/npm/useUserPackages.ts
  • app/composables/useGlobalSearch.ts
  • app/composables/useSettings.ts
  • test/e2e/interactions.spec.ts
  • test/fixtures/npm-registry/search/@vue.json
✅ Files skipped from review due to trivial changes (2)
  • app/composables/npm/useSearch.ts
  • test/fixtures/npm-registry/search/@vue.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/composables/npm/useOrgPackages.ts
  • test/e2e/interactions.spec.ts
  • app/composables/npm/useUserPackages.ts
  • app/composables/useGlobalSearch.ts

return
}
router.push({
void router.push({
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.

are these voids necessary?

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.

SSR hydration mismatch and "no results" flash when searchProvider is set to npm

2 participants