diff --git a/skills/instantsearch/SKILL.md b/skills/instantsearch/SKILL.md index 0cdb2d1..928e18d 100644 --- a/skills/instantsearch/SKILL.md +++ b/skills/instantsearch/SKILL.md @@ -10,7 +10,7 @@ description: >- license: MIT metadata: author: algolia - version: '1.0' + version: '1.1' --- # InstantSearch @@ -29,7 +29,10 @@ InstantSearch comes in three libraries. Detect which one applies from the projec Once identified, use the matching reference directory (e.g., `references/react/`) for all technology rules, anti-patterns, styling, glossary, and pattern-specific guidance. -> **Note:** This skill currently has references for **React InstantSearch** only. If the project uses Vue or vanilla JS, let the user know and ask how they'd like to proceed. +> **Note:** Coverage by library: +> +> - **React InstantSearch**: full coverage (autocomplete, search results page, custom widgets, middleware, SSR). +> - **Vue InstantSearch** and **InstantSearch.js**: library-level rules and autocomplete only. For other patterns, follow the [Source-of-truth check](#2-source-of-truth-check) and ask the user before scaffolding non-trivial flows. ## Workflow @@ -59,15 +62,54 @@ If starting fresh, go step by step, waiting for the user's answer before moving Do not proceed until you have credentials, schema, rendering preferences, and an understanding of the project's design. -### 2. Build +### 2. Source-of-truth check -This skill currently covers **autocomplete**. If the user asks for a different search pattern (e.g., a search results page, faceted search), let them know and ask how they'd like to proceed. +This skill bakes guidance for the highest-value patterns only. Whenever you need a widget, hook, connector, prop, middleware, future flag, or API that is **not explicitly documented in this skill**, you must consult the source of truth before writing code. Training data is stale, lossy, and frequently wrong on prop shapes. + +Follow the library's source-of-truth reference for the exact commands and URL patterns: + +| Library | Reference | +| ------- | ---------------------------------------------------------- | +| React | [source-of-truth.md](references/react/source-of-truth.md) | +| Vue | [source-of-truth.md](references/vue/source-of-truth.md) | +| JS | [source-of-truth.md](references/js/source-of-truth.md) | + +The reference always covers, in order: + +1. **Read installed types** in `node_modules` for prop shapes, renderState, and connector contracts. +2. **Fetch the live Algolia docs** at the canonical URL pattern. +3. **Grep installed CSS / source** for class names you intend to style. +4. **Only then write code.** If a step fails (offline, 404, missing types), ask the user before guessing. + +Do this even when a pattern reference exists, if you need any prop or behavior the pattern reference does not explicitly cover. + +### 3. Build + +Pick the matching pattern reference for the library and the user's request. If no pattern reference exists for what the user asked for, fall back on the source-of-truth check (Step 2) and ask the user before scaffolding speculative widget trees. Patterns available for each library: -| Pattern | React | Vue | JS | -| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | --- | -| Autocomplete | [features](references/react/autocomplete/features.md), [styling](references/react/autocomplete/styling.md), [anti-patterns](references/react/autocomplete/anti-patterns.md) | — | — | +| Pattern | React | Vue | JS | +| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Autocomplete | [features](references/react/autocomplete/features.md), [styling](references/react/autocomplete/styling.md), [anti-patterns](references/react/autocomplete/anti-patterns.md) | [features](references/vue/autocomplete/features.md), [styling](references/vue/autocomplete/styling.md), [anti-patterns](references/vue/autocomplete/anti-patterns.md) | [features](references/js/autocomplete/features.md), [styling](references/js/autocomplete/styling.md), [anti-patterns](references/js/autocomplete/anti-patterns.md) | +| Search results page (incl. faceted search, sort, pagination) | [features](references/react/search-results-page/features.md), [styling](references/react/search-results-page/styling.md), [anti-patterns](references/react/search-results-page/anti-patterns.md) | — | — | + +Also read and apply the library-level references (apply regardless of pattern): + +| Reference | React | Vue | JS | +| ---------------- | ----------------------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------- | +| Technology rules | [technology-rules.md](references/react/technology-rules.md) | [technology-rules.md](references/vue/technology-rules.md) | [technology-rules.md](references/js/technology-rules.md) | +| Anti-patterns | [anti-patterns.md](references/react/anti-patterns.md) | [anti-patterns.md](references/vue/anti-patterns.md) | [anti-patterns.md](references/js/anti-patterns.md) | +| Styling | [styling.md](references/react/styling.md) | [styling.md](references/vue/styling.md) | [styling.md](references/js/styling.md) | +| Glossary | [glossary.md](references/react/glossary.md) | [glossary.md](references/vue/glossary.md) | [glossary.md](references/js/glossary.md) | +| Source of truth | [source-of-truth.md](references/react/source-of-truth.md) | [source-of-truth.md](references/vue/source-of-truth.md) | [source-of-truth.md](references/js/source-of-truth.md) | +| Custom widgets | [custom-widgets.md](references/react/custom-widgets.md) | — | — | +| Middleware | [middleware.md](references/react/middleware.md) | — | — | +| SSR | [ssr.md](references/react/ssr.md) | — | — | +| Pattern | React | Vue | JS | +| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --- | --- | +| Autocomplete | [features](references/react/autocomplete/features.md), [styling](references/react/autocomplete/styling.md), [anti-patterns](references/react/autocomplete/anti-patterns.md) | — | — | +| Search results page (incl. faceted search, sort, pagination) | [features](references/react/search-results-page/features.md), [styling](references/react/search-results-page/styling.md), [anti-patterns](references/react/search-results-page/anti-patterns.md) | — | — | Also read and apply the library-level references (apply regardless of pattern): @@ -77,13 +119,17 @@ Also read and apply the library-level references (apply regardless of pattern): | Anti-patterns | [anti-patterns.md](references/react/anti-patterns.md) | — | — | | Styling | [styling.md](references/react/styling.md) | — | — | | Glossary | [glossary.md](references/react/glossary.md) | — | — | +| Source of truth | [source-of-truth.md](references/react/source-of-truth.md) | — | — | +| Custom widgets | [custom-widgets.md](references/react/custom-widgets.md) | — | — | +| Middleware | [middleware.md](references/react/middleware.md) | — | — | +| SSR | [ssr.md](references/react/ssr.md) | — | — | -If you are unsure how to implement a feature with built-in widgets, ask the user. Do not fall back to legacy libraries or custom connectors. +Consult types and live docs first (Step 2). Ask the user only if both fail. Never fall back to legacy libraries or guessed APIs. -### 3. Style +### 4. Style Follow the library's styling guide step by step, then apply the pattern-specific styling guide. Match the site's existing CSS methodology, color scheme, typography, spacing, and component patterns. -### 4. Review +### 5. Review Review your work against the library's anti-patterns, the pattern-specific anti-patterns, and the features checklist. Fix any violations. diff --git a/skills/instantsearch/evals/evals.json b/skills/instantsearch/evals/evals.json index 4918394..179032f 100644 --- a/skills/instantsearch/evals/evals.json +++ b/skills/instantsearch/evals/evals.json @@ -75,6 +75,112 @@ "Matches the dark theme", "Does not guess ais-* class names" ] + }, + { + "id": 6, + "prompt": "Add a search results page at /search for my Next.js App Router site. Users should be able to filter by brand and category, sort by price ascending or descending, and paginate. The index has replicas products_price_asc and products_price_desc.", + "expected_output": "A results page using InstantSearchNext with the canonical widget tree, refinements, sort, pagination, routing, insights, and a no-results boundary, with all non-trivial props sourced from installed types", + "files": [], + "expectations": [ + "Uses InstantSearchNext from react-instantsearch-nextjs at the layout level, not wrapping a single page", + "Sets routing={true} and insights={true} on the provider", + "Uses Configure to set hitsPerPage, attributesToHighlight, and attributesToSnippet", + "Uses RefinementList for brand and HierarchicalMenu (or RefinementList/Menu, justified) for category", + "Uses SortBy with the provided replica index names (products, products_price_asc, products_price_desc)", + "Uses Pagination, not InfiniteHits, since the user did not ask for infinite scroll", + "Renders a NoResultsBoundary using useInstantSearch().results.nbHits", + "Uses Highlight on hit text attributes; does not render raw strings", + "Reads RefinementList and SortBy props from node_modules/react-instantsearch/dist/es/widgets before writing prop values it has not used before", + "Does NOT add getServerState (App Router handles SSR via InstantSearchNext)", + "Does NOT combine Pagination with InfiniteHits" + ] + }, + { + "id": 7, + "prompt": "I want a searchable refinement list for brand (so the user can type to filter brands) and active filter chips that show as removable pills at the top of the results.", + "expected_output": "RefinementList with searchable + searchablePlaceholder configured from the type, plus CurrentRefinements wired with widget-aware classNames for the chip layout", + "files": [], + "expectations": [ + "Reads RefinementList .d.ts to confirm the searchable, searchablePlaceholder, limit, and showMore props before using them", + "Adds searchable={true} and a searchablePlaceholder on RefinementList", + "Uses CurrentRefinements (not a custom hook) for the active-filter chips", + "Pairs CurrentRefinements with ClearRefinements where appropriate, with excludedAttributes considered when needed", + "Greps installed source for ais-CurrentRefinements-* class names before writing chip CSS", + "Does not rebuild a refinement list with useRefinementList when RefinementList suffices" + ] + }, + { + "id": 8, + "prompt": "Add SSR to my React InstantSearch results page. I'm using Next.js Pages Router (not App Router).", + "expected_output": "Pages Router SSR using getServerState plus InstantSearchSSRProvider, with the same widget tree on server and client", + "files": [], + "expectations": [ + "Uses getServerState from react-instantsearch/server inside getServerSideProps", + "Wraps the widget tree in InstantSearchSSRProvider with serverState", + "Passes renderToString from react-dom/server into getServerState", + "Configures routing with a serverUrl (or framework equivalent) so the server-side router can resolve URL state", + "Declares the searchClient at module scope, not inside getServerSideProps", + "Renders the same widget tree on server and client", + "Does NOT use InstantSearchNext (that's App Router only)", + "Reads getServerState.d.ts and InstantSearchSSRProvider.d.ts from node_modules before wiring props" + ] + }, + { + "id": 9, + "prompt": "I need a custom widget that renders the available brands as round colored chips with each brand's logo, instead of the default checkbox list. Selecting a chip refines the results.", + "expected_output": "A custom widget built with useRefinementList that renders chips, with renderState fields read from the hook's installed type", + "files": [], + "expectations": [ + "Uses useRefinementList from react-instantsearch (not useConnector with a raw connector, since a hook exists)", + "Reads node_modules/react-instantsearch/dist/es/connectors/useRefinementList.d.ts to learn the return type before destructuring", + "Calls refine(item.value) on chip click", + "Renders accessibility attributes (role, aria-pressed) explicitly since the built-in widget's accessibility is lost", + "Considers transformItems on RefinementList as a lighter-weight alternative before going custom", + "Does NOT rebuild refinement state from useHits + useState" + ] + }, + { + "id": 10, + "prompt": "Set up my React InstantSearch project with the correct future flags so it's ready for the next major release.", + "expected_output": "Future flags applied based on installed types and the live Upgrade guides, not training-data recall", + "files": [], + "expectations": [ + "Reads InstantSearchProps['future'] from node_modules/react-instantsearch types to enumerate available flags", + "Fetches https://www.algolia.com/doc/guides/building-search-ui/upgrade-guides/react to confirm current recommended flags and defaults", + "Applies flags on the InstantSearch (or InstantSearchNext) provider with values matching the installed package version", + "Does NOT enable a flag that does not appear in the installed type", + "Asks the user before enabling any flag whose default cannot be determined from types or docs" + ] + }, + { + "id": 11, + "prompt": "Add an autocomplete search bar to my Vue 3 app. I'm using vue-instantsearch elsewhere on the site for a results page. The autocomplete should query my products index and clicking a result should navigate to /products/[objectID].", + "expected_output": "Autocomplete built with @algolia/autocomplete-js mounted from a Vue component (onMounted/onBeforeUnmount), reusing the module-level searchClient", + "files": [], + "expectations": [ + "Picks @algolia/autocomplete-js as the autocomplete library, NOT a vue-instantsearch widget such as or ", + "Does NOT treat @algolia/autocomplete-js as an anti-pattern (it is the recommended path for Vue, unlike React)", + "Mounts autocomplete inside a Vue component via onMounted with a ref to the host element, and calls instance.destroy() in onBeforeUnmount", + "Declares the searchClient at module scope (outside the component), reusing the same one used by vue-instantsearch where applicable", + "Configures getSources with an Algolia source that uses algoliasearch v5's search method shape", + "Wires getItemUrl and a templates.item that links to /products/[objectID]", + "Confirms the autocomplete options shape (getSources, templates) against the live Algolia docs or installed types before scaffolding" + ] + }, + { + "id": 12, + "prompt": "Add autocomplete search to a vanilla JS site. The page already mounts an InstantSearch.js results page. The autocomplete should sit in the header and link to product detail pages.", + "expected_output": "Autocomplete built with @algolia/autocomplete-js, separate from the instantsearch.js widget tree, sharing the same module-level searchClient", + "files": [], + "expectations": [ + "Picks @algolia/autocomplete-js as the autocomplete library, NOT the instantsearch.js searchBox widget for autocomplete behavior", + "Does NOT treat @algolia/autocomplete-js as an anti-pattern (it is the recommended path for vanilla JS, unlike React)", + "Calls autocomplete({ container, getSources, ... }) once and saves the returned instance for later destroy() on teardown", + "Reuses the module-level searchClient already used by instantsearch.js, declared outside any function", + "Uses algoliasearch v5's search method shape inside getSources", + "Imports @algolia/autocomplete-theme-classic (or documents the chosen theme) so the dropdown is styled", + "Confirms the autocomplete options shape against the live Algolia docs or installed types before scaffolding" + ] } ] } diff --git a/skills/instantsearch/references/js/anti-patterns.md b/skills/instantsearch/references/js/anti-patterns.md new file mode 100644 index 0000000..db25599 --- /dev/null +++ b/skills/instantsearch/references/js/anti-patterns.md @@ -0,0 +1,16 @@ +# Anti-patterns (InstantSearch.js) + +These supplement the [technology rules](technology-rules.md). For anything not listed here, run the [Source-of-truth check](source-of-truth.md). + +| Anti-pattern | Why it's wrong | What to do instead | +| ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Calling `instantsearch({...})` more than once on the same page | Multiple instances duplicate searches and conflict over routing | Create one instance in module scope; share it across mount points | +| Calling `search.start()` more than once | Re-initializes lifecycle; widgets get registered/disposed unpredictably | Call `start()` once after all initial widgets are added; mount more later via `addWidgets` | +| Building search by hitting `algoliasearch` directly | Bypasses widget tree, routing, and analytics | Use widgets via `search.addWidgets([...])` | +| Hardcoding an index name like `instant_search` | Demo index from training data | Ask the user | +| Guessing widget options (`refinementList({ searchable, showMore, ... })`) | Option names and accepted values change between versions | Read the widget's `.d.ts` from `node_modules/instantsearch.js/es/widgets/` before using | +| Guessing `ais-*` class names | CamelCase, easy to miss | Grep `node_modules/instantsearch.js` for the actual class | +| Re-implementing `pagination`, `refinementList`, `sortBy` with connectors and DOM | Loses URL sync, accessibility, and the widget's edge-case handling | Use the widget; reach for the connector only when rendering genuinely cannot use templates | +| Mixing autocomplete with `searchBox` to build search-as-you-type | Two state owners; flaky | Use `@algolia/autocomplete-js` for autocomplete dropdowns; `searchBox` only for results-page input | +| Skipping `templates` / `cssClasses` and styling via DOM-position selectors | Brittle, breaks on minor version updates | Use the widget's `templates` and `cssClasses` options; target `ais-*` classes in CSS | +| Forgetting to `dispose` widgets you removed dynamically | Search keeps refining for the removed widget; URL state stays | Use `search.removeWidgets([...])`; reach for the dispose method on the widget if needed | \ No newline at end of file diff --git a/skills/instantsearch/references/js/autocomplete/anti-patterns.md b/skills/instantsearch/references/js/autocomplete/anti-patterns.md new file mode 100644 index 0000000..387ab35 --- /dev/null +++ b/skills/instantsearch/references/js/autocomplete/anti-patterns.md @@ -0,0 +1,13 @@ +# Autocomplete Anti-patterns (InstantSearch.js) + +These are in addition to the [JS anti-patterns](../anti-patterns.md). Note: some anti-patterns from the React skill **do not apply** in vanilla JS because the recommended path is different. + +| Anti-pattern | Why it's wrong | What to do instead | +| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Building autocomplete with `searchBox` + `hits` and a custom dropdown | Reinvents `@algolia/autocomplete-js` poorly; loses keyboard nav, A11y, plugins | Use `@algolia/autocomplete-js` per [features.md](features.md) | +| Forgetting `instance.destroy()` on SPA route change / teardown | Leaks DOM and event listeners; double-mount on hot reload causes duplicate panels | Save the `autocomplete(...)` return value and call `destroy()` at teardown | +| Recreating the `searchClient` for every page render | Loses the request cache; thrashes network | Declare `searchClient` in module scope and import it | +| Recreating the `autocomplete(...)` instance on every reactive update | Tears down and rebuilds the dropdown; flicker | Mount once; update via `instance.setQuery`, `setActiveItemId`, or option closures | +| Guessing `getSources` shape from training data | The shape evolves; `getItems` return values, `templates`, plugin contracts change | Read installed `@algolia/autocomplete-js` types and the live doc before writing | +| Mixing `instantsearch({...})` widgets and `@algolia/autocomplete-js` over the same input | Two state owners on one input; flaky behavior | Pick one: autocomplete-js for the dropdown experience, `searchBox` for results-page input | +| Styling `ais-*` classes when the rendered DOM is autocomplete-js (`aa-*`) | Selectors don't match; styles silently noop | Style `aa-*` classes for autocomplete-js panels; `ais-*` only for InstantSearch widgets | \ No newline at end of file diff --git a/skills/instantsearch/references/js/autocomplete/features.md b/skills/instantsearch/references/js/autocomplete/features.md new file mode 100644 index 0000000..8f7b050 --- /dev/null +++ b/skills/instantsearch/references/js/autocomplete/features.md @@ -0,0 +1,57 @@ +# Autocomplete (InstantSearch.js) + +InstantSearch.js does not ship a dedicated autocomplete widget. The recommended path is **`@algolia/autocomplete-js`**, which is a separate UI library that integrates with `algoliasearch` v5. This is **not** an anti-pattern in vanilla JS (it is in React). + +Before implementing, run the [Source-of-truth check](../source-of-truth.md) and confirm the recommended path against the live docs. + +## Recommended path: `@algolia/autocomplete-js` + +The pattern is: + +1. Install `@algolia/autocomplete-js` and `algoliasearch` v5. +2. Create or pick a host element (selector or DOM node). +3. Call `autocomplete({ container, ... })` once. +4. Configure `getSources` to query the same Algolia index used elsewhere; reuse the module-level `searchClient`. + +```ts +import { autocomplete } from "@algolia/autocomplete-js"; +import { liteClient as algoliasearch } from "algoliasearch/lite"; +import "@algolia/autocomplete-theme-classic"; + +const searchClient = algoliasearch(process.env.ALGOLIA_APP_ID!, process.env.ALGOLIA_API_KEY!); + +const instance = autocomplete({ + container: "#autocomplete", + placeholder: "Search...", + getSources: () => [ + { + sourceId: "products", + getItems: ({ query }) => + searchClient.search({ + requests: [{ indexName: "YOUR_INDEX", query, hitsPerPage: 5 }], + }).then((res) => res.results[0].hits), + getItemUrl: ({ item }) => `/products/${item.objectID}`, + templates: { + item: ({ item, html }) => html`${item.name}`, + }, + }, + ], +}); +``` + +Confirm the `autocomplete` options shape (`getSources`, `templates`, `plugins`) and the `algoliasearch` v5 `search` method against installed types and the live docs before scaffolding. Both have evolved. + +When the page is destroyed (SPA route change, panel teardown), call `instance.destroy()` to release listeners. + +## Alternative: `searchBox` widget for non-autocomplete inputs + +If the user wants a search input on a results page (no dropdown of suggestions), use the `searchBox` widget from `instantsearch.js/es/widgets`. This is not autocomplete; it's the search input for an InstantSearch widget tree. Don't confuse the two. + +## Multiple sources, recent searches, query suggestions + +Same building blocks as the React autocomplete: query suggestions index, recent-searches plugin, multi-source `getSources` array. Read `https://www.algolia.com/doc/ui-libraries/autocomplete/api-reference/autocomplete-js` for the up-to-date plugin and source APIs. + +## Live docs + +- Autocomplete-js getting started: `https://www.algolia.com/doc/ui-libraries/autocomplete/introduction/getting-started` +- API reference: `https://www.algolia.com/doc/ui-libraries/autocomplete/api-reference/autocomplete-js` diff --git a/skills/instantsearch/references/js/autocomplete/styling.md b/skills/instantsearch/references/js/autocomplete/styling.md new file mode 100644 index 0000000..f69de7d --- /dev/null +++ b/skills/instantsearch/references/js/autocomplete/styling.md @@ -0,0 +1,28 @@ +# Autocomplete Styling (InstantSearch.js) + +Because the recommended path uses `@algolia/autocomplete-js`, styling targets `aa-*` classes (the autocomplete-js vocabulary), not `ais-*`. Verify by grepping: + +```bash +rg -o 'aa-[A-Za-z]+' node_modules/@algolia/autocomplete-js | sort -u +rg -o 'aa-[A-Za-z]+' node_modules/@algolia/autocomplete-theme-classic | sort -u +``` + +Common targets: `aa-Autocomplete`, `aa-Form`, `aa-Input`, `aa-Panel`, `aa-PanelLayout`, `aa-Source`, `aa-SourceHeader`, `aa-Item`, `aa-ItemContent`, `aa-DetachedOverlay`, `aa-DetachedFormContainer`, `aa-DetachedContainer`. Names occasionally evolve; always grep first. + +## Starting point: classic theme + +Importing `@algolia/autocomplete-theme-classic` gives you a baseline. Override on top with the project's tokens; do not duplicate the theme. + +## Layout invariants (for a custom theme) + +- Panel positioned relative to the input; the library handles this when `container` is correct. +- Panel constrained with `max-height` and `overflow-y: auto` to avoid viewport overflow. +- Hover and keyboard-selected states styled on `aa-Item[aria-selected="true"]` so pointer and keyboard users see the same active item. + +## Mobile / detached behavior + +`@algolia/autocomplete-js` supports a detached mode via the `detachedMediaQuery` option. Set it to match the project's mobile breakpoint and style the detached overlay. The structural shape mirrors the React detached overlay (full-screen container, sticky form area, scrollable panel); reuse the rules from the [React autocomplete styling guide](../../react/autocomplete/styling.md), substituting `aa-*` selectors for `ais-Autocomplete*` selectors. + +## Tailwind v4 + +`aa-*` and `ais-*` classes are rendered at runtime. Place their CSS outside any `@layer` directive. diff --git a/skills/instantsearch/references/js/glossary.md b/skills/instantsearch/references/js/glossary.md new file mode 100644 index 0000000..28a2f83 --- /dev/null +++ b/skills/instantsearch/references/js/glossary.md @@ -0,0 +1,14 @@ +# Glossary (InstantSearch.js) + +Terms the AI must use correctly. Cross-cutting Algolia terms (Index, Replica, Facet, Refinement, Filter) match the [React glossary](../react/glossary.md). This file lists JS-specific additions. + +| Term | Meaning | Watch out for | +|---|---|---| +| **Widget (JS)** | An object returned by a factory like `refinementList({...})`. Mounts via `search.addWidgets([...])` and unmounts via `removeWidgets`. | Widgets are imperative here; do not assume React/Vue lifecycle. | +| **Container** | The DOM element (CSS selector or element reference) where a widget renders, passed via the `container` option. | Required for visible widgets. The DOM must exist before `search.start()` for widgets registered up front. | +| **Connector** | A factory like `connectRefinementList(renderFn)` that returns a custom widget bound to your render function. The escape hatch for fully custom DOM. | Use when the built-in widget's `templates` cannot express the design. | +| **`templates`** | Per-widget option for replacing rendered HTML. Accepts strings, functions, or tagged templates. | Read the widget's type for accepted keys; signatures differ per widget. | +| **`cssClasses`** | Per-widget option for appending custom classes to the default `ais-*` ones. | Keys differ per widget; type-check before using. | +| **`search.start()`** | Boots the InstantSearch instance once all initial widgets are registered. | Call once. Add more widgets later with `addWidgets`. | +| **`getInitialResults`** | The SSR helper. Walks the widget tree, performs initial searches, returns serializable state. | Pair with `initialUiState` / hydration on the client. Read the source for the current API. | +| **Insights (JS)** | Click/conversion analytics, enabled via `insights: true` in `instantsearch({...})`. | Same Algolia Insights as the React/Vue skill. | diff --git a/skills/instantsearch/references/js/source-of-truth.md b/skills/instantsearch/references/js/source-of-truth.md new file mode 100644 index 0000000..b41b90c --- /dev/null +++ b/skills/instantsearch/references/js/source-of-truth.md @@ -0,0 +1,54 @@ +# Source of Truth (InstantSearch.js) + +Training data is stale on widget options, connector contracts, and recently shipped APIs. Before using any widget, connector, middleware, or option that is **not explicitly documented in this skill**, complete the steps below in order. + +## 1. Read installed types + +```bash +ls node_modules/instantsearch.js/es \ + node_modules/instantsearch.js/es/widgets \ + node_modules/instantsearch.js/es/connectors \ + node_modules/instantsearch.js/es/lib \ + node_modules/instantsearch.js/es/types +``` + +What to read, by use case: + +| You need to know | Read | +| ------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| `instantsearch(options)` options | `node_modules/instantsearch.js/es/lib/InstantSearch.d.ts` (or `index.d.ts`) | +| Widget options (`refinementList`, `pagination`, ...)| `node_modules/instantsearch.js/es/widgets//.d.ts` | +| Connector contract (custom widgets) | `node_modules/instantsearch.js/es/connectors//connect.d.ts` | +| Middleware contract | `node_modules/instantsearch.js/es/types/middleware.d.ts` | +| Routing types | `node_modules/instantsearch.js/es/types/router.d.ts` and `lib/routers/history.d.ts` | +| SSR helpers | `node_modules/instantsearch.js/es/lib/server/getInitialResults.d.ts` | + +If files have moved, grep: + +```bash +rg -l "${SymbolName}" node_modules/instantsearch.js +``` + +## 2. Fetch the live Algolia docs + +| Topic | URL pattern | +| --------------------------- | ---------------------------------------------------------------------------- | +| Widget reference | `https://www.algolia.com/doc/api-reference/widgets//js` | +| Guides | `https://www.algolia.com/doc/guides/building-search-ui//js` | +| Custom widgets | `https://www.algolia.com/doc/guides/building-search-ui/widgets/create-your-own-widgets/js` | +| Routing | `https://www.algolia.com/doc/guides/building-search-ui/going-further/routing-urls/js` | +| SSR | `https://www.algolia.com/doc/guides/building-search-ui/going-further/server-side-rendering/js` | + +Widget slugs are kebab-case: `refinementList` (the JS factory) -> `refinement-list` (the URL). + +## 3. Grep installed CSS for class names + +```bash +rg -o 'ais-[A-Za-z]+(-[a-z]+)?' node_modules/instantsearch.js | sort -u +``` + +`ais-*` class names match the React and Vue packages. Names are CamelCase; do not guess. + +## 4. Only then write code + +If any step fails, stop and ask the user. Do not guess widget option names, connector return values, or routing types. diff --git a/skills/instantsearch/references/js/styling.md b/skills/instantsearch/references/js/styling.md new file mode 100644 index 0000000..f8f1a2b --- /dev/null +++ b/skills/instantsearch/references/js/styling.md @@ -0,0 +1,52 @@ +# Styling InstantSearch.js Widgets + +Style widgets by writing CSS targeting `ais-*` selectors in the project's global stylesheet. Use the project's design tokens. The class-name vocabulary matches React InstantSearch and Vue InstantSearch (all three packages share the rendering layer). + +## Finding class names + +```bash +rg -o 'ais-[A-Za-z]+(-[a-z]+)?' node_modules/instantsearch.js | sort -u +``` + +Names are CamelCase (e.g., `ais-Pagination-list`). Do not guess. + +## Per-widget overrides via `cssClasses` + +InstantSearch.js widgets accept a `cssClasses` option that adds custom classes alongside the defaults. Use it when the project has a CSS architecture (BEM, utility-first) that wants its own selectors: + +```ts +refinementList({ + container: "#brand", + attribute: "brand", + cssClasses: { + list: "my-refinement-list", + item: "my-refinement-list__item", + }, +}); +``` + +Read the widget's `.d.ts` for the keys accepted in `cssClasses`. They differ per widget. + +## Per-widget overrides via `templates` + +Widgets accept a `templates` option for custom HTML rendering. Templates can be strings, functions returning HTML strings, or `html` tagged-template helpers. When you replace a template, the inner DOM is yours; outer `ais-*` classes still apply for layout selectors. + +```ts +hits({ + container: "#hits", + templates: { + item: (hit, { html, components }) => + html`

${components.Highlight({ hit, attribute: "name" })}

`, + }, +}); +``` + +The `html` and `components` helpers come from the template render context. Read the widget's type for the exact signature in the installed version. + +## Tailwind v4 + +`ais-*` classes are rendered at runtime. Place their CSS outside any `@layer` directive to avoid Tailwind v4 purging. + +## Pattern-specific styling + +This skill covers autocomplete only at the pattern level. For other patterns, run the source-of-truth check. diff --git a/skills/instantsearch/references/js/technology-rules.md b/skills/instantsearch/references/js/technology-rules.md new file mode 100644 index 0000000..979a746 --- /dev/null +++ b/skills/instantsearch/references/js/technology-rules.md @@ -0,0 +1,26 @@ +# Technology Rules (InstantSearch.js) + +These rules apply to **all** InstantSearch.js patterns. Coverage in this skill is limited to library-level rules and autocomplete; for other patterns, follow the [Source-of-truth check](source-of-truth.md) and ask the user before scaffolding non-trivial flows. + +## Always + +- **Before using any widget, connector, option, middleware, or router that is not explicitly documented in this skill, complete the [Source-of-truth check](source-of-truth.md).** Do not write code from training-data recall alone. +- If `instantsearch.js` and `algoliasearch` are not already installed, install the latest versions and **wait for the install to complete** before proceeding. Use the project's package manager. If the project uses a CDN (jsDelivr, unpkg with script tags), follow that pattern instead. +- Use `algoliasearch` v5. For browser bundles, prefer the lite client: `import { liteClient as algoliasearch } from "algoliasearch/lite";` +- **Declare the search client in module scope** (or a single shared module) so it is reused across the page. The reference must remain stable. +- Create the search instance once with `const search = instantsearch({ ... })` and call `search.start()` once. Do not recreate the instance on framework re-renders. +- Mount widgets via `search.addWidgets([ refinementList({...}), hits({...}), ... ])`. The factories are imported from `instantsearch.js/es/widgets`. +- Use the `highlight` widget / template helper on displayed text and `snippet` for long text. Set `attributesToHighlight` and `attributesToSnippet` via the `configure` widget so the response carries the markup. +- Use the `configure` widget for `hitsPerPage` and other index params. +- Prefer built-in widgets and template options over connectors when the widget can achieve the design. +- Set `insights: true` in `instantsearch({ insights: true, ... })` for click analytics. +- **Set up routing** by passing `routing: true` (or a routing object with `router` and `stateMapping`) in `instantsearch({...})`. Read the router type for full configuration. + +## Never + +- **Never use v3 syntax in a v4+ project.** Widget option names changed; copy-paste from older docs will break silently. +- **Never call `search.start()` more than once** on the same instance. +- **Never guess credentials or index names.** Always ask the user. +- **Never mix `instantsearch.js` and `react-instantsearch` / `vue-instantsearch` in the same widget tree.** Pick one library per page. +- **Never overuse connectors.** If the built-in widget with `cssClasses` and `templates` works, use it. Connectors are for genuinely custom rendering. +- **Never guess connector renderState shape.** Read `connect.d.ts` from `node_modules/instantsearch.js/es/connectors/` before destructuring. diff --git a/skills/instantsearch/references/react/anti-patterns.md b/skills/instantsearch/references/react/anti-patterns.md index 67882d4..8d9e315 100644 --- a/skills/instantsearch/references/react/anti-patterns.md +++ b/skills/instantsearch/references/react/anti-patterns.md @@ -4,11 +4,19 @@ These apply across **all** React InstantSearch patterns. They supplement the tec If you catch yourself doing any of these, stop and reconsider: -| Anti-pattern | Why it's wrong | What to do instead | -| ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -| Using `` in a Next.js App Router project | Missing SSR/RSC integration | Use `` from `react-instantsearch-nextjs` | -| Using `react-instantsearch-router-nextjs` with App Router | Requires Pages Router's `singletonRouter` | Use the `routing` prop on `` directly | -| Building search with `algoliasearch` client directly | Bypasses the widget tree and state management | Use InstantSearch widgets | -| Hardcoding an index name like `instant_search` | Likely a demo index from training data | Ask the user | -| Styling widgets with arbitrary selectors (tag names, positional selectors, generated classes) | Brittle, breaks on updates | Write CSS targeting `ais-*` selectors. See the styling guide | -| Guessing `ais-*` class names without checking | Names are CamelCase and easy to get wrong. Broken styles with no error | Grep `node_modules/react-instantsearch` for actual class names before writing CSS | +| Anti-pattern | Why it's wrong | What to do instead | +| --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| Using `` in a Next.js App Router project | Missing SSR/RSC integration | Use `` from `react-instantsearch-nextjs` | +| Using `react-instantsearch-router-nextjs` with App Router | Requires Pages Router's `singletonRouter` | Use the `routing` prop on `` directly | +| Building search with `algoliasearch` client directly | Bypasses the widget tree and state management | Use InstantSearch widgets | +| Hardcoding an index name like `instant_search` | Likely a demo index from training data | Ask the user | +| Styling widgets with arbitrary selectors (tag names, positional selectors, generated classes) | Brittle, breaks on updates | Write CSS targeting `ais-*` selectors. See the styling guide | +| Guessing `ais-*` class names without checking | Names are CamelCase and easy to get wrong. Broken styles with no error | Grep `node_modules/react-instantsearch` for actual class names before writing CSS | +| Rebuilding ``, ``, ``, `` from `useHits` + local state | Reimplements behavior the widget already handles (URL sync, refinements API, accessibility) | Use the widget. Reach for the corresponding hook only when the rendering shape genuinely cannot be styled | +| Guessing prop shapes for refinement widgets (`limit`, `showMore`, `showMoreLimit`, `searchable`, `searchablePlaceholder`, `transformItems`, `operator`, `sortBy`) | Prop names and accepted values change between versions; silent runtime breakage | Read the widget's `.d.ts` from `node_modules/react-instantsearch/dist/es/widgets/` first | +| Writing a custom `useConnector` hook when a built-in widget + `classNames` would do | Adds maintenance burden and re-renders; loses built-in accessibility and routing wiring | Use the widget. Custom widgets are for cases the built-in widget genuinely cannot render. See [custom-widgets.md](custom-widgets.md) | +| Using middleware to sync state to the URL | `routing` already does this; middleware will conflict with it | Configure `routing={true}` (or `routing={{ router, stateMapping }}`). See [middleware.md](middleware.md) | +| Using middleware to set default refinements or query params | These belong in `` or refinement widget defaults, not in middleware | Use `` for index params; use the widget's default-selection prop where supported | +| Destructuring `useInstantSearch()` / `useXxx()` return values from training-data recall | `renderState` field names and nesting change across versions | Read the hook's return type before destructuring | +| Adding `getServerState` on top of `` in App Router | App Router SSR is already handled by ``; doubling up causes hydration mismatches | Drop `getServerState` for App Router. Use it only for Pages Router / non-Next React with SSR. See [ssr.md](ssr.md) | +| Enabling `future.*` flags from training-data recall | Flags change defaults and semantics; defaults flip across releases | Read `InstantSearchProps['future']` from installed types and the Upgrade guides before enabling | diff --git a/skills/instantsearch/references/react/custom-widgets.md b/skills/instantsearch/references/react/custom-widgets.md new file mode 100644 index 0000000..d7fca51 --- /dev/null +++ b/skills/instantsearch/references/react/custom-widgets.md @@ -0,0 +1,73 @@ +# Custom Widgets (React) + +This file is for the cases the built-in widgets genuinely cannot do. Most "custom widget" instincts are wrong: a built-in widget plus `classNames` plus a small `transformItems` callback covers the large majority of real designs. + +## Decide first: do you actually need a custom widget? + +Walk this checklist before reaching for `useConnector` / `useXxx`. If any of these resolves the problem, do not write a custom widget. + +- Is the issue purely visual? Use the widget's `classNames` prop and write `ais-*` CSS. See [styling.md](styling.md). +- Do you need to filter, reorder, or annotate items? Use the widget's `transformItems` (most refinement widgets accept it). Read the widget's `.d.ts` for the exact signature. +- Do you need a different DOM wrapper? Use the widget's `classNames` plus normal CSS, or wrap the widget in your own component. The widget's children DOM is supported via class targeting. +- Do you need a non-default rendering for `Hits` items only? Use ``'s `hitComponent` / `itemComponent` prop. Don't reach for `useHits` for this. +- Do you need to render a refinement widget you've never used before? Run the [Source-of-truth check](source-of-truth.md) first; most "missing" features are existing props you didn't know about. + +If none of the above applies, you have a real custom-widget case. Continue. + +## When custom widgets are justified + +- Genuinely custom interaction (e.g., a 2D color/refinement picker, a map-based geo refiner, a slider that snaps to non-numeric values). +- Composite UI that needs state from multiple connectors at once and where rendering them as separate widgets would split the layout. +- Headless contexts (e.g., rendering refinements inside a third-party component library that takes data, not children). +- Programmatic search-state inspection that has no widget equivalent (read-only `useInstantSearch()` is the answer here, not a new widget). + +## The contract + +There are two paths. Pick based on the API surface available. + +### Path A: `useXxx` hook (preferred when one exists) + +`react-instantsearch` ships a hook for every connector: `useSearchBox`, `useHits`, `useRefinementList`, `useHierarchicalMenu`, `useRange`, `usePagination`, `useSortBy`, `useStats`, `useToggleRefinement`, `useCurrentRefinements`, `useClearRefinements`, `useConfigure`, `useInstantSearch`, and so on. + +Workflow: + +1. Read the hook's `.d.ts` from `node_modules/react-instantsearch/dist/es/connectors/use.d.ts` to learn the **arguments** (connector params) and the **return type** (renderState). +2. Call the hook inside a component nested under `` / ``. +3. Render whatever you want from the returned state. Forward `classNames`, refs, accessibility attributes manually if you want them. + +Do **not** assume field names. The return type is the source of truth. Names like `items`, `refine`, `currentRefinement`, `canRefine`, `isShowingMore`, `toggleShowMore` exist on some connectors and not others. + +### Path B: `useConnector(connector, params)` (when there is no React hook) + +Use this when you need a connector exposed by `instantsearch.js` that does not yet have a dedicated React hook, or when you are wrapping a custom connector you wrote yourself. + +```ts +import { useConnector } from "react-instantsearch"; +import connectFoo from "instantsearch.js/es/connectors/foo/connectFoo"; +import type { + FooConnectorParams, + FooWidgetDescription, +} from "instantsearch.js/es/connectors/foo/connectFoo"; + +export function useFoo(params: FooConnectorParams) { + return useConnector(connectFoo, params); +} +``` + +The two type parameters come from the connector's `.d.ts`. Read that file before writing the wrapper. The shape returned is the connector's renderState type, minus the lifecycle plumbing (`init`, `render`, etc., are handled by `useConnector`). + +For the official guide, read `https://www.algolia.com/doc/guides/building-search-ui/widgets/create-your-own-widgets/react`. + +## Common pitfalls + +- **Stale state in event handlers.** Refinement callbacks (`refine`) returned from a hook are stable across renders, but values you destructure (`currentRefinement`, `items`) are not. Read them inside the handler or via a ref if you need the latest. +- **Re-render storms.** A custom widget that calls `refine` on every keystroke without debouncing will trigger a search per keystroke. For typed inputs, debounce or use ``'s `queryHook` prop. +- **Lost accessibility.** Built-in widgets render correct ARIA semantics. Custom widgets do not. Add `role`, `aria-*`, keyboard handlers explicitly. +- **Multiple instances of the same connector with conflicting params.** Calling `useRefinementList({ attribute: "brand", limit: 10 })` and `useRefinementList({ attribute: "brand", limit: 20 })` in two components creates two virtual widgets that will fight. Use one source of truth, or accept that only the first registered params win. +- **Reading `useInstantSearch().renderState` for a connector you didn't mount.** `renderState` only contains entries for connectors actually mounted in the tree (via widgets or hooks). If your connector isn't mounted, the renderState entry will be missing. Mount the hook (even in a hidden component) or use the connector directly. + +## Do not + +- Do not re-implement ``, ``, ``, ``, ``, ``, or `` from `useHits` + `useState`. Each has a dedicated hook with the right state. +- Do not subclass or extend the InstantSearch widgets. They are not designed for inheritance. +- Do not reach into `instantsearch.js`'s internals from a React component. Stay at the connector / hook layer. diff --git a/skills/instantsearch/references/react/glossary.md b/skills/instantsearch/references/react/glossary.md index ef697ff..ee644c3 100644 --- a/skills/instantsearch/references/react/glossary.md +++ b/skills/instantsearch/references/react/glossary.md @@ -12,3 +12,11 @@ Terms the AI must use correctly. Misusing these leads to wrong code or wrong lib | **Facet** | An attribute configured for filtering in the index settings. Used with widgets like `` or ``. | Not every attribute is a facet. Only those configured as `attributesForFaceting` in the index settings. | | **Refinement** | A filter actively applied by the user (e.g., selecting "Nike" in a brand facet). | "Refinement" is the Algolia/InstantSearch term. The AI may say "filter". That's fine conversationally, but use "refinement" in code (e.g., ``, ``). | | **Filter** | A broader term. In Algolia, filters can be set programmatically via `` or by the user via refinement widgets. | Don't confuse programmatic filters (set by the developer) with refinements (set by the user). | +| **renderState** | The shape returned by a connector / `useXxx` hook (and exposed under `useInstantSearch().renderState`). Field names depend on the connector and version. | Never destructure from training-data recall. Read the hook's `.d.ts` before pulling fields. | +| **InstantSearch instance** | The object returned by `useInstantSearch()` (also accessible as `instance` in middleware). Holds search state, helper, status, results, and `renderState`. | Not a React state object. Mutating it directly is unsupported. Drive it via widgets, hooks, or middleware. | +| **Middleware** | A pluggable hook into the InstantSearch lifecycle (`subscribe`, `started`, `unsubscribe`, `onStateChange`). Used for analytics, custom URL params, side effects. | Not a place to re-implement what `routing` or `` already does. See [middleware.md](middleware.md). | +| **getServerState / SSR provider** | `getServerState` (from `react-instantsearch`) walks the tree, performs initial searches, returns serializable state. `` re-hydrates it on the client. Used for Next.js Pages Router / Remix / other React SSR. | Not needed for App Router (`` handles SSR). Don't combine the two. See [ssr.md](ssr.md). | +| **Virtual widget** | A widget that registers search parameters with the InstantSearch instance without rendering UI (e.g., a `useSearchBox()` hook called in a parent that doesn't render the search input). | Useful for SSR or for splitting state from rendering. Not the same as ``, which is itself effectively a virtual widget. | +| **Dynamic widgets** | A pattern (``) that renders refinement widgets based on facets returned by the search response, using `attributesForFaceting` `searchable(...)` markers and `facetOrdering`. | Read the live doc before using; the rendering rules depend on index `renderingContent` settings. | +| **Future flags** | Opt-in switches under `` that toggle upcoming default behaviors (e.g., `preserveSharedStateOnUnmount`). | Defaults flip between releases. Read `InstantSearchProps['future']` and the Upgrade guides before enabling. | +| **Insights** | Algolia's click/conversion analytics, enabled via `insights={true}` on ``. | Not the same as the `algolia-mcp` analytics queries. This is in-app event tracking, not reporting. | diff --git a/skills/instantsearch/references/react/middleware.md b/skills/instantsearch/references/react/middleware.md new file mode 100644 index 0000000..5d5f5e2 --- /dev/null +++ b/skills/instantsearch/references/react/middleware.md @@ -0,0 +1,68 @@ +# Middleware (React) + +Middleware is a low-level extension point on the InstantSearch instance. It is the right tool for a narrow set of cross-cutting concerns and the wrong tool for almost everything else. Read this before reaching for `addMiddlewares` or the `middleware` prop. + +## When to use middleware + +Use middleware only when **all** of these are true: + +- The behavior is cross-cutting (touches every search, every state change, or the whole lifecycle), not a single widget concern. +- It cannot be expressed via ``, `routing`, a widget prop, or `useInstantSearch()` in a small component. +- It needs to observe (and optionally react to) state changes, not just render. + +Legitimate examples: + +- **Custom analytics tagging** beyond what `insights={true}` covers (e.g., funnel events, A/B exposure, custom event names). +- **Syncing search state to a non-URL store** (e.g., a session-only Zustand store, a server-side log, a parent app's state machine). +- **Custom URL parameters** that `routing.stateMapping` cannot express (rare; try `stateMapping` first). +- **Instrumentation** for debugging (logging every state transition during development). + +## When NOT to use middleware + +The most common middleware misuses: + +- **URL sync.** Use `routing` (or `routing={{ router, stateMapping }}`). Middleware that writes to `window.location` will fight `routing`. +- **Default refinements / default query.** Use `` for index params (`filters`, `query`, `hitsPerPage`) or the refinement widget's default-selection prop. Do not push state via middleware on init. +- **Reacting to a single widget's state.** Use the widget's hook (`useRefinementList`, `useSortBy`) inside a small component. Middleware is too coarse. +- **Triggering side effects on hit clicks.** That is a render-time concern. Use the click handler in your `hitComponent` and call `sendEvent` from `useInsights()` if needed. +- **Replacing `routing` because the URL format isn't what you want.** Customize via `routing.stateMapping` first; that's exactly what it's for. + +## Contract + +Read the live, version-correct contract from the installed types: `node_modules/instantsearch.js/es/types/middleware.d.ts`. The signature is stable across recent releases but field names occasionally evolve, so check before writing one. + +The shape is roughly: + +```ts +type Middleware = (options: { instantSearchInstance: InstantSearch }) => { + subscribe?: () => void; + started?: () => void; + unsubscribe?: () => void; + onStateChange?: (params: { uiState: UiState }) => void; +}; +``` + +Hook semantics: + +- `subscribe` runs once when the middleware is registered. Set up listeners here. +- `started` runs after the InstantSearch instance starts. Safe to read `instantSearchInstance.status`. +- `onStateChange` runs on every UI-state transition. Keep it cheap. This is hot. +- `unsubscribe` runs when the middleware is removed or the instance is disposed. Clean up. + +## Registering middleware in React + +Use the `middleware` prop on `` / ``, or `useInstantSearch().addMiddlewares(...)` from a child component. Read the props type to confirm the exact name and accepted shape (an array of middleware factories) for the installed version. + +When using `useInstantSearch().addMiddlewares` inside a component, return the unsubscribe function from `useEffect` to avoid duplicate registrations on rerenders. + +## Pitfalls + +- **Stale `uiState` reads.** `onStateChange` receives the current `uiState` as its argument. Do not read `instantSearchInstance.getUiState()` inside; the argument is what you want. +- **Synchronous heavy work in `onStateChange`.** Every refinement, query keystroke, and pagination click triggers it. Debounce or schedule with `queueMicrotask` for non-urgent work. +- **Middleware that mutates `uiState`.** Middleware is observational by default. To affect search, call `instantSearchInstance.setUiState(...)` or use widgets/hooks. Do not mutate the argument. +- **Middleware order.** Middleware runs in registration order. If two middlewares race on the same external store, the later registration wins per cycle. Keep them independent. +- **Forgetting `unsubscribe`.** Long-lived listeners (window events, intervals, store subscriptions) will leak across remounts if you do not return them in `unsubscribe`. + +## Doc reference + +For the canonical, version-correct guide, fetch `https://www.algolia.com/doc/api-reference/widgets/middleware/react`. Confirm any prop or method name there before shipping. diff --git a/skills/instantsearch/references/react/search-results-page/anti-patterns.md b/skills/instantsearch/references/react/search-results-page/anti-patterns.md new file mode 100644 index 0000000..9c68f9f --- /dev/null +++ b/skills/instantsearch/references/react/search-results-page/anti-patterns.md @@ -0,0 +1,19 @@ +# Search Results Page Anti-patterns + +These are in addition to the [library-level anti-patterns](../anti-patterns.md). + +| Anti-pattern | Why it's wrong | What to do instead | +| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| Adding a refinement widget for an attribute not in `attributesForFaceting` | Runtime error: "facet not configured" | Confirm the index's faceting config first. Ask the user to add the attribute via `algolia-cli` if missing | +| Combining `` and `` on the same index | Both widgets manage page state; they fight | Pick one. `` for shareable URLs, `` for catalog scroll | +| Adding `` without verified replicas | Switching to a missing replica throws on first selection | Confirm replica index names with the user or via `algolia-cli` before adding the widget | +| Skipping a no-results component | Page renders nothing when the user mistypes; looks broken | Render a no-results boundary using `useInstantSearch().results.nbHits` (see features.md) | +| Putting refinement widgets outside their target `` in a multi-index page | Refinements apply to the wrong index (the parent) | Place each refinement widget inside the `` it should refine | +| Using `` to set refinement defaults the user can change | `` is for fixed index params; user-changeable defaults belong on the refinement widget | Use the widget's default-selection prop (read its type), or set `uiState` via `routing.stateMapping` | +| Hardcoding `hitsPerPage` in the URL via middleware | Conflicts with `routing` and the `` widget | Use `` for a fixed value, `` for a user-chosen value | +| Re-implementing `` from `useHits` + a manual page counter | Loses URL sync, accessibility, and edge-case handling (last page, single page, no results) | Use ``. If the rendering is custom, use `usePagination` and read its return type | +| Wrapping each result column section in its own `` provider | Each provider creates a separate search instance; widgets inside don't see siblings | One provider at the page (or layout) level, multiple widgets and `` blocks inside | +| Mounting the same refinement widget twice with the same `attribute` and different `limit`s | Two virtual widgets fight; only one set of params wins | Mount once. If desktop/mobile differ, render the same widget instance via CSS, not two instances | +| Calling `setUiState` from `useEffect` to "preselect" refinements | Causes an extra search per mount and fights `routing` | Use the widget's default-selection prop, or `routing.stateMapping` to derive defaults from the URL | +| Showing raw text for `name` / `title` on hits | Loses query highlighting; users can't see why a hit matched | Use `` per technology rules | +| Long descriptions rendered with `` instead of `` | Renders the full text with marks; visually noisy and slow | Use `` and set `attributesToSnippet` in `` (e.g., `description:30` for 30 words) | \ No newline at end of file diff --git a/skills/instantsearch/references/react/search-results-page/features.md b/skills/instantsearch/references/react/search-results-page/features.md new file mode 100644 index 0000000..ba2f381 --- /dev/null +++ b/skills/instantsearch/references/react/search-results-page/features.md @@ -0,0 +1,179 @@ +# Search Results Page (React) + +A search results page is the page users land on after submitting a query (from autocomplete, a header search box, or a deep link). It typically combines: a search input, a hits list, pagination, refinements (facets), sort, stats, and a no-results state. It is also the natural home for faceted search. + +This file scaffolds the structural skeleton. **Prop details and any prop you have not used before must come from the [Source-of-truth check](../source-of-truth.md), not from this file.** + +## Discovery (in addition to SKILL.md Discover step) + +Confirm before scaffolding: + +- **Route**: which URL renders this page? Verify it actually reads query params and renders dynamic results (not a static listing). +- **Refinement attributes**: which record fields should become facets? They must be in the index's `attributesForFaceting`. If the user is unsure, ask before adding refinement widgets to avoid a "facet is not configured" error at runtime. +- **Sort replicas**: does the index have replicas (e.g., `_price_asc`, `_price_desc`)? If yes, ask the user to confirm the replica names and labels for the sort dropdown. If no, do not add `` (or ask whether they want to set replicas up via the `algolia-cli` skill first). +- **Pagination vs. infinite scroll**: ask the user. Both are supported. Pagination is the default for shareable URLs; infinite scroll is common for catalog-style UX. +- **Empty/error states**: what should the page show when there are no hits, no query, or the network fails? Ask if not obvious from the design. +- **Mobile refinements**: how should refinements appear on mobile? Common pattern: drawer or modal, opened by a "Filters" button. Ask if the design isn't explicit. + +## Canonical widget tree + +This is the structural skeleton. Resolve any non-trivial prop via the [Source-of-truth check](../source-of-truth.md). + +```tsx +import { + Configure, + CurrentRefinements, + ClearRefinements, + HierarchicalMenu, + Hits, + HitsPerPage, + Pagination, + RangeInput, + RefinementList, + SearchBox, + SortBy, + Stats, + ToggleRefinement, + useInstantSearch, +} from "react-instantsearch"; + +function SearchResultsPage() { + return ( + <> + +
+ + + + +
+ + + +
+ }> + + + +
+ + ); +} + +function NoResultsBoundary({ children, fallback }: { children: React.ReactNode; fallback: React.ReactNode }) { + const { results } = useInstantSearch(); + if (!results.__isArtificial && results.nbHits === 0) { + return <>{fallback}; + } + return <>{children}; +} + +function NoResults() { + const { indexUiState } = useInstantSearch(); + return
No results for "{indexUiState.query}".
; +} + +function Hit({ hit }: { hit: Record & { objectID: string } }) { + // Render with per technology-rules.md + return null; +} +``` + +The `` shape is the version-stable pattern from the official guide. Confirm `useInstantSearch` return fields before destructuring (per the intro source-of-truth rule). + +## Refinement widgets: which one when + +| Use case | Widget | Notes | +| ------------------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| Multi-select categorical filter | `` | For long lists, look up `searchable`, `searchablePlaceholder`, `limit`, `showMore`, `showMoreLimit`. | +| Single-select categorical filter | `` | Mutually exclusive selection. Often used with `transformItems` for ordering. | +| Hierarchical / nested categories | `` | Requires `lvl0/lvl1/lvl2` attributes in the records. Read the guide. | +| Numeric range with input fields | `` | Numeric attribute. Two inputs (min/max). | +| Numeric range with a slider | (no built-in) | Use `useRange` and your component library's slider. Read [custom-widgets.md](../custom-widgets.md). | +| Boolean toggle (free shipping, etc.) | `` | Toggles a single facet value on/off. | +| Active-filter chips | `` | Renders all active refinements as removable chips. Useful at the top of results. | +| Clear-all button | `` | Pairs with ``. Look up `excludedAttributes` to keep some refinements pinned. | + +## Pagination vs. infinite scroll + +Two mutually exclusive options: + +- ``: shareable URL state per page. Default for results pages. +- ``: replaces both `` and ``. Loads more on scroll or on a "Load more" button. Look up `showPrevious` and `translations` if you need them, and verify the widget's interaction with `routing`. + +## Sort + +`` switches the active index between the primary and its replicas. The replicas must already exist on the Algolia side; this widget only chooses among them. + +```tsx + +``` + +If replicas don't exist, do not fabricate them. Tell the user they need to be created first (point them to the `algolia-cli` skill). + +## URL state, sharing, deep linking + +`routing={true}` on the provider already syncs SearchBox query, refinements, page, and sort to the URL. Verify by changing a refinement and reloading; state should persist. + +For custom URL formats (e.g., `/search/brand/nike` rather than `?brand=nike`), pass `routing={{ router, stateMapping }}` and read both types. This is non-trivial; consult the live [Routing guide](https://www.algolia.com/doc/guides/building-search-ui/going-further/routing-urls/react) before scaffolding. + +## Multi-index results pages + +If the page shows results from more than one index (e.g., products + articles), nest `` blocks. Each index gets its own ``, refinements, hits, and pagination, scoped to that index. + +```tsx + + + + + + + + + + + + + + + +``` + +Refinement widgets target the **enclosing index**. Putting `` outside any `` refines the root index; putting it inside `` refines only that one. + +## Features checklist + +Consider each. Include when the index and use case support it: + +- [ ] `` sets `hitsPerPage`, `attributesToHighlight`, `attributesToSnippet` +- [ ] `` (or wired to autocomplete that submits here) +- [ ] `` for "X results in Y ms" +- [ ] `` (or ``) with a typed `hitComponent` using `` / `` +- [ ] `` (or ``, not both) +- [ ] `` + `` +- [ ] One or more refinement widgets matching the index's `attributesForFaceting` +- [ ] `` if replicas exist +- [ ] `` if the design needs it +- [ ] No-results boundary with a helpful message +- [ ] Mobile refinement drawer / modal +- [ ] `routing={true}` and `insights={true}` on the provider +- [ ] SSR if the framework supports it (see [ssr.md](../ssr.md)) + +For pattern-specific styling, see [styling.md](styling.md). For pattern-specific anti-patterns, see [anti-patterns.md](anti-patterns.md). diff --git a/skills/instantsearch/references/react/search-results-page/styling.md b/skills/instantsearch/references/react/search-results-page/styling.md new file mode 100644 index 0000000..40781b8 --- /dev/null +++ b/skills/instantsearch/references/react/search-results-page/styling.md @@ -0,0 +1,70 @@ +# Search Results Page Styling + +This covers results-page-specific layout. For the general approach, see the [styling guide](../styling.md). Always grep `node_modules` for actual `ais-*` class names before writing CSS; do not guess. + +## Page-level layout + +A typical results page has three regions: a header (search box, stats, sort), a refinements panel (sidebar on desktop, drawer on mobile), and a results column (hits + pagination). Use the host site's grid/flex tokens; the InstantSearch widgets do not impose a layout. + +What matters at the layout level: + +- The **refinements panel** should not collapse or overflow when refinement lists grow. Set a `max-height` with `overflow-y: auto` per panel section, and a sticky header above each panel. +- The **results column** should reserve a minimum height so a transition from "many results" to "few results" doesn't shift content. +- The **search box** should keep a visible focus state and match the autocomplete input from the rest of the site. + +## Refinement panel + +Each refinement widget renders a labeled list. Common targets: + +- `ais-RefinementList` (root), `ais-RefinementList-list`, `ais-RefinementList-item`, `ais-RefinementList-label`, `ais-RefinementList-checkbox`, `ais-RefinementList-labelText`, `ais-RefinementList-count`, `ais-RefinementList-showMore`. +- `ais-HierarchicalMenu` follows the same shape, with nested `ais-HierarchicalMenu-list` for child levels. +- `ais-CurrentRefinements` renders chips: `ais-CurrentRefinements-list`, `ais-CurrentRefinements-item`, `ais-CurrentRefinements-label`, `ais-CurrentRefinements-category`, `ais-CurrentRefinements-categoryLabel`, `ais-CurrentRefinements-delete`. +- `ais-ClearRefinements-button`. + +Verify all of these against the installed source before writing CSS. Names occasionally change. + +Common rules: + +- Hide the count when zero (`ais-RefinementList-count` with conditional styling) only if the design demands it. +- Style `:hover`, `:focus-visible`, and the checked state on `ais-RefinementList-checkbox` consistently. +- Reserve right padding on items so long labels do not collide with the count. + +## Hits grid + +`` renders an `ais-Hits` root, an `ais-Hits-list`, and `ais-Hits-item` per hit. The list is a `
    ` by default; style it with grid or flex on the host site's tokens. The hit component you provide is rendered inside `ais-Hits-item`; its inner DOM is yours. + +For consistent height, set a fixed image aspect ratio on the hit image and clamp text lines (`-webkit-line-clamp` or the host's utility) on titles and descriptions. + +## Pagination + +`ais-Pagination`, `ais-Pagination-list`, `ais-Pagination-item`, `ais-Pagination-link`, plus modifier classes (`ais-Pagination-item--selected`, `ais-Pagination-item--disabled`, `ais-Pagination-item--firstPage`, `ais-Pagination-item--lastPage`, `ais-Pagination-item--previousPage`, `ais-Pagination-item--nextPage`, `ais-Pagination-item--page`). + +Style the modifier classes explicitly. The `--selected` state must be visually distinct from hover. + +## Mobile refinements drawer + +The widgets do not ship a drawer. Common pattern: + +1. Render the refinements panel inside a `` or a custom modal component the project already has. +2. Toggle it via a "Filters" button shown only at the mobile breakpoint. +3. Inside the drawer, render the same refinement widgets you render on desktop. Do not duplicate `` providers. + +If the project already has a drawer/sheet component, use it. Do not introduce a new modal pattern just for refinements. + +## Sort and HitsPerPage + +`ais-SortBy` and `ais-HitsPerPage` render a `