Skip to content

Conversation

@alexdln
Copy link
Contributor

@alexdln alexdln commented Feb 10, 2026

There was a hydration error that caused re-rendering and repeated requests.

There were three problems:

  1. For Algolia search, if a query was processed on the server or retrieved from the cache, there was no place on the client to add suggestions, so they disappeared. Fixed it by returning them to useLazyAsyncData and, when ready, I re-assign them into suggestions ref.
  2. For search method configuration - we simply didn't know the user's preferred method on the server. So the server always worked with algolia, and the client could then rehydrate with npm. Fixed by storing this section additionally in a cookie and reusing it from there
  3. npm suggestions had a similar issue to the first one: the logic was only executed on the server, but never executed on the client and, as a result, server send suggestion, but client missed this information (that's why this option doesn't work at all in production right now). Refactored the npm suggestions retrieval to "useLazyAsyncData" as well

@vercel
Copy link

vercel bot commented Feb 10, 2026

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

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Feb 11, 2026 2:26am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Feb 11, 2026 2:26am
npmx-lunaria Ignored Ignored Feb 11, 2026 2:26am

Request Review

@codecov
Copy link

codecov bot commented Feb 10, 2026

Codecov Report

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

Files with missing lines Patch % Lines
app/composables/npm/useSearch.ts 69.69% 28 Missing and 2 partials ⚠️
app/components/Header/SearchBox.vue 21.42% 8 Missing and 3 partials ⚠️
app/components/SearchProviderToggle.client.vue 35.71% 8 Missing and 1 partial ⚠️
app/pages/index.vue 0.00% 2 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@userquin
Copy link
Contributor

why at search page with nuxt on input, pressing enter navigates to /package/nuxt? maybe because exact search entry found?

@alexdln alexdln marked this pull request as ready for review February 11, 2026 00:27
@alexdln
Copy link
Contributor Author

alexdln commented Feb 11, 2026

@userquin looks like the same behavior in production now already, I can look at it as the next issue (tomorrow 🫠)

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 11, 2026

📝 Walkthrough

Walkthrough

This PR refactors the npm search composable to standardise its return payload: searchResponse, suggestions and packageAvailability are returned together via a new emptySearchPayload helper. suggestionRequestId is converted to a ref to track concurrent suggestion requests. Algolia and NPM search branches, early-return/error paths, fetchMore and initial-data integration were aligned to the consolidated payload shape. A reactive npmSuggestions path and package-availability syncing were added. Separately, useSettings now persists the search provider to a secure cookie and reads from that cookie when determining the provider.

Possibly related PRs

Suggested reviewers

  • danielroe
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description is directly related to the changeset, explaining the hydration errors being fixed and mapping each code change to specific problems addressed.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

Copy link
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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/composables/npm/useSearch.ts (1)

440-448: Variable shadowing: npmSuggestions parameter obscures the outer useLazyAsyncData const.

The destructured parameter npmSuggestions at line 442 shadows the npmSuggestions constant declared at line 427. While technically correct (the outer is only used for the watch source), this reduces readability.

♻️ Suggested rename for clarity
   watch(
     [() => asyncData.data.value.suggestions, () => npmSuggestions.data.value.suggestions],
-    ([algoliaSuggestions, npmSuggestions]) => {
-      if (algoliaSuggestions.length || npmSuggestions.length) {
-        suggestions.value = algoliaSuggestions.length ? algoliaSuggestions : npmSuggestions
+    ([algoliaSuggestions, npmSuggestionsData]) => {
+      if (algoliaSuggestions.length || npmSuggestionsData.length) {
+        suggestions.value = algoliaSuggestions.length ? algoliaSuggestions : npmSuggestionsData
       }
     },
     { immediate: true },
   )

Copy link
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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/composables/npm/useSearch.ts (1)

443-443: Inconsistent optional chaining on asyncData.data.value.

Line 443 uses asyncData.data.value.suggestions (no optional chaining) while line 454 uses asyncData.data.value?.packageAvailability (with optional chaining). Since emptySearchPayload is provided as the default, asyncData.data.value should never be null—consider using consistent access patterns for clarity.

Also applies to: 454-454

Comment on lines 118 to 123
const cookie = useCookie('search-provider', {
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 30,
path: '/',
})
Copy link
Member

Choose a reason for hiding this comment

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

ideally we wouldn't have a cookie here, so that we could cache the search page (by query key).

I think we could show algolia data in initial load, but refresh on client-side with npm data if that's what they prefer.

wdyt?

Copy link
Contributor Author

@alexdln alexdln Feb 11, 2026

Choose a reason for hiding this comment

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

This will be a very slow behavior. It will first download the full HTML, then clean everything up and start loading npm...

It feels like in this case - this option will be an example of a bad experience and will rather harm the service than give the user an option

Copy link
Contributor Author

@alexdln alexdln Feb 11, 2026

Choose a reason for hiding this comment

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

But I had one pretty crazy idea of ​​adding the provider to the query before this approach. Is that suitable for us?

So, if the user has npm selected by default, every time they visit the search page, the provider will be added along with the query. But this won't work if they go directly to the search...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. This setting is now just a preference. If the user accesses the search via any mechanism within the site, the provider will also be added to the URL. Otherwise, the page will continue to use algolia until the user switches to npm.

Copy link
Member

Choose a reason for hiding this comment

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

hm. so I was thinking that algolia is enabled by default, so this is the edge case of the user preferring npm and hard reloading the search page rather than using client-side navigation.

the other reason was that algolia is much faster and only requires one http request rather than three, so we are more performant by skipping the npm search.

(but I guess if you have npmx as a search engine in the browser, and prefer npm, you might hit this more often than I originally expected)

we could also prerender the search page and do the fetching entirely on the client side. this would mean no danger of mismatch, very fast initial response, no double fetching....

Copy link
Contributor Author

@alexdln alexdln Feb 11, 2026

Choose a reason for hiding this comment

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

Yes, definitely, rare case, but sometimes the results don't load and the user reloads the page and gets one more bad experience.

I think the current solution is the most optimal in terms of scenario coverage. What do you think about latest changes?

UPD: updated a bit, now it works as I planned and is unnoticeable for the user

Copy link
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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/composables/npm/useSearch.ts (1)

442-463: Inconsistent optional chaining between watchers.

Line 443 accesses asyncData.data.value.suggestions without optional chaining, line 454 uses asyncData.data.value?.packageAvailability with optional chaining, and line 455 accesses npmSuggestions.data.value.packageAvailability without it.

While this is safe at runtime due to the default values, the inconsistency reduces readability. Consider standardising the access pattern.

♻️ Suggested consistency improvement
   watch(
-    [() => asyncData.data.value.suggestions, () => npmSuggestions.data.value.suggestions],
+    [() => asyncData.data.value?.suggestions ?? [], () => npmSuggestions.data.value?.suggestions ?? []],
     ([algoliaSuggestions, npmSuggestionsValue]) => {
       if (algoliaSuggestions.length || npmSuggestionsValue.length) {
         suggestions.value = algoliaSuggestions.length ? algoliaSuggestions : npmSuggestionsValue
       }
     },
     { immediate: true },
   )

   watch(
     [
-      () => asyncData.data.value?.packageAvailability,
-      () => npmSuggestions.data.value.packageAvailability,
+      () => asyncData.data.value?.packageAvailability ?? null,
+      () => npmSuggestions.data.value?.packageAvailability ?? null,
     ],

Comment on lines +362 to +365
if (!intent || !name) {
suggestionsLoading.value = false
return { suggestions: [], packageAvailability: availability }
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Early return may bypass awaiting the package existence check.

When the query is a valid scoped package name like @scope/package, isValidNewPackageName returns true (line 345), but parseSuggestionIntent returns { intent: null, name: '' } because of the / check. This causes the early return at line 363 to execute before Promise.all(promises) at line 400.

The returned packageAvailability will be null even though a check was initiated. The reactive ref is eventually updated via the side effect in the .then() callback, so the final UI state is correct, but the return value from validateSuggestionsNpm is inconsistent with actual state.

🛠️ Proposed fix to await promises before early return
   if (!intent || !name) {
+    if (promises.length > 0) {
+      await Promise.all(promises)
+    }
     suggestionsLoading.value = false
     return { suggestions: [], packageAvailability: availability }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!intent || !name) {
suggestionsLoading.value = false
return { suggestions: [], packageAvailability: availability }
}
if (!intent || !name) {
if (promises.length > 0) {
await Promise.all(promises)
}
suggestionsLoading.value = false
return { suggestions: [], packageAvailability: availability }
}

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.

3 participants