Skip to content

feat: add feeds widgets for JS, React, and Vue#6969

Merged
drodriguln merged 31 commits intofeat/composition-multifeedfrom
feat/composition-multifeed-layer3-widgets
Apr 30, 2026
Merged

feat: add feeds widgets for JS, React, and Vue#6969
drodriguln merged 31 commits intofeat/composition-multifeedfrom
feat/composition-multifeed-layer3-widgets

Conversation

@e-krebs
Copy link
Copy Markdown
Contributor

@e-krebs e-krebs commented Apr 13, 2026

Summary

Layer 3 of the Multifeed Composition Support (RFC). Stacked on feat/composition-multifeed-layer2-connector (PR #6964).

Adds user-facing feeds widgets for all three flavors. Each widget owns FeedContainer lifecycle (create, register, remove, reorder) — the connector only computes feedIDs.

InstantSearch.js

  • feeds() widget with per-feed DOM containers, widget scoping, and DOM reordering
  • Exported from both main and UMD entrypoints

React InstantSearch

  • <Feeds> component with per-feed IndexContext.Provider scoping
  • Accepts a renderFeed={({ feedID }) => ...} render prop (not children) — consistent with other customization props in the library (hitComponent, itemComponent, etc.)
  • useFeeds hook wrapping connectFeeds via useConnector
  • FeedContainer creation + registration fused in render body for SSR compat

Vue InstantSearch

  • <ais-feeds> component with per-feed AisFeedProvider (provide/inject)
  • Container reconciliation in watcher (side-effect-free render)

Shared patterns across all flavors

  • Deferred removal with coalesced single timer
  • Unmount/dispose merges active + pending containers for clean teardown

Test plan

  • 11 JS widget unit tests + 1 integration test
  • 5 React unit tests + 2 integration tests (real InstantSearch + SSR)
  • 5 Vue unit tests + 1 integration test
  • Export parity test updated
  • TypeScript: `yarn run type-check:v3` and `yarn run type-check:v4`
  • Lint: `yarn lint:changed`
  • CI green

🤖 Generated with Claude Code

e-krebs and others added 16 commits April 8, 2026 15:32
Compositions can return multiple result sets (feeds), each identified by
a `feedID`. Previously, `_runComposition` spliced only 1 result per
derived helper, discarding additional feeds. This changes `queriesCount`
to `Infinity` so all feeds are captured, and builds a `_feedResults` map
and `_feedOrder` array on the primary `SearchResults` for downstream
consumption by `connectFeeds` (Layer 2).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…Infinity

Use undefined queriesCount to signal "take all results" in _dispatchAlgoliaResponse,
avoiding the Infinity hack. When queriesCount is undefined (composition path), use
results directly; when defined (regular search path), splice as before.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The feedID, _feedResults, and _feedOrder type definitions have no consumers
yet. Defer the typing decision (interface vs class, naming) to Layer 2 when
connectFeeds introduces the first consumer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… map

Replace _feedResults (Record<string, SearchResults>) and _feedOrder
(string[]) with a single lastResults.feeds (CompositionSearchResults[])
ordered array. This simplifies the API and ensures feeds survive the
SSR getInitialResults → JSON → hydration round-trip naturally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Create a separate SearchResults instance for lastResults instead of
reusing feeds[0], which caused circular references during
JSON.stringify (lastResults.feeds[0] === lastResults).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Haroen Viaene <hello@haroen.me>
Move storeRenderState from module-private in index.ts to render-args.ts
so FeedContainer can reuse it in the feeds connector (Layer 2).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…dContainer

Introduce indexWidgetTypes const and IndexWidgetType as single source of
truth for index-like widget types. Update isIndexWidget, IndexWidgetDescription,
BuiltinTypes, and BuiltinWidgetTypes to use them. Add 'ais.feeds' and
'ais.feedContainer' to builtin type unions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The metadata middleware hardcoded 'ais.index' instead of using
isIndexWidget, so widgets inside feed containers were invisible
to the Algolia Crawler metadata.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ized flag

Replace `instantSearchInstance.started` check with a local `initialized` flag
so that child widgets added before `container.init()` are not eagerly
initialized. This prevents double-init when the parent index tree calls
`init()` on the container after widgets have already been added.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…omputation

Remove FeedContainer lifecycle management from the connector — widget layers
now own container creation, registration, and cleanup. The connector only
computes feedIDs from results.feeds and exposes them as render state.

- Remove `widgets` param and internal `feedContainers` map
- Change render state from `feeds: [{ feedID, container }]` to `feedIDs: string[]`
- Make `getWidgetRenderState` stateless (derives feedIDs from results)
- Simplify `dispose()` to only call `unmountFn()`
- Extract shared test helpers to `test/createFeedsTestHelpers.ts`

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented Apr 13, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 276 complexity · 67 duplication

Metric Results
Complexity 276
Duplication 67

View in Codacy

TIP This summary will be updated as you push new changes.

- Use `import type` for type-only imports
- Fix import ordering in test files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@e-krebs e-krebs force-pushed the feat/composition-multifeed-layer3-widgets branch from 0bdc7d6 to 29a3827 Compare April 13, 2026 16:12
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 13, 2026

More templates

algoliasearch-helper

npm i https://pkg.pr.new/algolia/instantsearch/algoliasearch-helper@6969

instantsearch-ui-components

npm i https://pkg.pr.new/algolia/instantsearch/instantsearch-ui-components@6969

instantsearch.css

npm i https://pkg.pr.new/algolia/instantsearch/instantsearch.css@6969

instantsearch.js

npm i https://pkg.pr.new/algolia/instantsearch/instantsearch.js@6969

react-instantsearch

npm i https://pkg.pr.new/algolia/instantsearch/react-instantsearch@6969

react-instantsearch-core

npm i https://pkg.pr.new/algolia/instantsearch/react-instantsearch-core@6969

react-instantsearch-nextjs

npm i https://pkg.pr.new/algolia/instantsearch/react-instantsearch-nextjs@6969

react-instantsearch-router-nextjs

npm i https://pkg.pr.new/algolia/instantsearch/react-instantsearch-router-nextjs@6969

vue-instantsearch

npm i https://pkg.pr.new/algolia/instantsearch/vue-instantsearch@6969

commit: dadf036

@e-krebs e-krebs force-pushed the feat/composition-multifeed-layer3-widgets branch 3 times, most recently from 1fd7ba0 to eff4746 Compare April 14, 2026 08:56
@e-krebs e-krebs marked this pull request as ready for review April 14, 2026 09:07
@e-krebs e-krebs requested review from a team, FabienMotte and aymeric-giraudet and removed request for a team April 14, 2026 09:07
Comment thread packages/instantsearch.js/src/widgets/feeds/feeds.ts
@e-krebs e-krebs force-pushed the feat/composition-multifeed-layer3-widgets branch from 6f2bae7 to 5d0261b Compare April 28, 2026 16:07
e-krebs and others added 10 commits April 28, 2026 18:26
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `feeds()` widget that creates per-feed DOM containers and manages
FeedContainer lifecycle. Each feed gets a scoped `<div class="ais-Feeds-feed">`
and its own FeedContainer registered with the parent index.

- DOM creation, reuse, reorder, and cleanup per feed
- Deferred removal with coalesced timer (matches connectDynamicWidgets pattern)
- Dispose merges active + pending containers for clean teardown
- Exported from both main and UMD entrypoints
- 11 unit tests + 1 integration test with real search lifecycle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add `<Feeds>` component that creates per-feed `IndexContext.Provider` scopes,
and `useFeeds` hook wrapping `connectFeeds` via `useConnector`.

- FeedContainer creation + registration fused in render body for SSR compat
- Deferred removal with coalesced timer (single ref pattern from useWidget)
- Unmount cleanup merges active + pending containers
- 5 unit tests + 2 integration tests (real InstantSearch + SSR)
- Exported from react-instantsearch-core

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add `<ais-feeds>` component with per-feed `AisFeedProvider` scoping via
Vue's provide/inject for `$_ais_getParentIndex`.

- Container reconciliation in watcher (side-effect-free render)
- Deferred removal with coalesced timer (single timer pattern)
- Unmount cleanup merges active + pending containers
- 5 unit tests + 1 integration test with real InstantSearch
- Exported as AisFeeds from vue-instantsearch
- Updated mock and index tests for compositionID support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…type safety

FeedContainer.render() ignores the argument internally but IndexWidget type
requires it. Pass renderOptionsRef to satisfy TypeScript.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@algolia/composition doesn't exist under algoliasearch v4.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… to renderFeed

Switch from a children render function to a dedicated `renderFeed` prop with
an object argument `({ feedID })`. Keeps the API consistent with other React
InstantSearch components (`hitComponent`, `itemComponent`, `bannerComponent`),
which all use explicit, non-children customization props.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…veWidgets

findIndex expects a predicate function, not the widget object — the call
threw at runtime the first time removeWidgets was invoked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@e-krebs e-krebs force-pushed the feat/composition-multifeed-layer3-widgets branch from 42ea90c to a4af193 Compare April 28, 2026 16:36
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 70 out of 81 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/instantsearch.js/src/widgets/feeds/feeds.ts
Comment thread packages/instantsearch.js/src/widgets/feeds/feeds.ts
Comment thread packages/vue-instantsearch/src/components/Feeds.js
Comment thread packages/react-instantsearch-core/src/components/Feeds.tsx
Comment thread examples/vue/e-commerce-with-composition/src/App.vue
@drodriguln drodriguln force-pushed the feat/composition-multifeed-layer3-widgets branch from 98bd1e8 to d874101 Compare April 29, 2026 13:22
@drodriguln drodriguln force-pushed the feat/composition-multifeed-layer3-widgets branch from d874101 to c6a2105 Compare April 29, 2026 13:32
Base automatically changed from feat/composition-multifeed-layer2-connector to feat/composition-multifeed April 30, 2026 15:42
@drodriguln drodriguln merged commit 468a1f0 into feat/composition-multifeed Apr 30, 2026
11 checks passed
@drodriguln drodriguln deleted the feat/composition-multifeed-layer3-widgets branch April 30, 2026 15:52
drodriguln added a commit that referenced this pull request Apr 30, 2026
* feat(helper): support multifeed composition responses

Compositions can return multiple result sets (feeds), each identified by
a `feedID`. Previously, `_runComposition` spliced only 1 result per
derived helper, discarding additional feeds. This changes `queriesCount`
to `Infinity` so all feeds are captured, and builds a `_feedResults` map
and `_feedOrder` array on the primary `SearchResults` for downstream
consumption by `connectFeeds` (Layer 2).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: bump bundlesize limits for multifeed helper changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(helper): omit queriesCount for composition instead of using Infinity

Use undefined queriesCount to signal "take all results" in _dispatchAlgoliaResponse,
avoiding the Infinity hack. When queriesCount is undefined (composition path), use
results directly; when defined (regular search path), splice as before.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(helper): remove composition type definitions, defer to layer 2

The feedID, _feedResults, and _feedOrder type definitions have no consumers
yet. Defer the typing decision (interface vs class, naming) to Layer 2 when
connectFeeds introduces the first consumer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(helper): use lastResults.feeds array instead of _feedResults map

Replace _feedResults (Record<string, SearchResults>) and _feedOrder
(string[]) with a single lastResults.feeds (CompositionSearchResults[])
ordered array. This simplifies the API and ensures feeds survive the
SSR getInitialResults → JSON → hydration round-trip naturally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(helper): avoid circular ref in multifeed lastResults

Create a separate SearchResults instance for lastResults instead of
reusing feeds[0], which caused circular references during
JSON.stringify (lastResults.feeds[0] === lastResults).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* apply suggestions from code review

Co-authored-by: Haroen Viaene <hello@haroen.me>

* refactor(instantsearch): extract storeRenderState to shared util

Move storeRenderState from module-private in index.ts to render-args.ts
so FeedContainer can reuse it in the feeds connector (Layer 2).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(instantsearch): centralize index widget types, widen for feedContainer

Introduce indexWidgetTypes const and IndexWidgetType as single source of
truth for index-like widget types. Update isIndexWidget, IndexWidgetDescription,
BuiltinTypes, and BuiltinWidgetTypes to use them. Add 'ais.feeds' and
'ais.feedContainer' to builtin type unions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(metadata): use isIndexWidget to recurse into feed containers

The metadata middleware hardcoded 'ais.index' instead of using
isIndexWidget, so widgets inside feed containers were invisible
to the Algolia Crawler metadata.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(instantsearch): add FeedContainer for per-feed widget subtrees

Made-with: Cursor

* feat(instantsearch): add connectFeeds connector for multifeed compositions

Made-with: Cursor

* test(instantsearch): add unit tests for FeedContainer and connectFeeds

Made-with: Cursor

* chore: bump bundlesize limits for multifeed connector

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(instantsearch): prevent double-init in FeedContainer with initialized flag

Replace `instantSearchInstance.started` check with a local `initialized` flag
so that child widgets added before `container.init()` are not eagerly
initialized. This prevents double-init when the parent index tree calls
`init()` on the container after widgets have already been added.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(instantsearch): simplify connectFeeds to stateless feedIDs computation

Remove FeedContainer lifecycle management from the connector — widget layers
now own container creation, registration, and cleanup. The connector only
computes feedIDs from results.feeds and exposes them as render state.

- Remove `widgets` param and internal `feedContainers` map
- Change render state from `feeds: [{ feedID, container }]` to `feedIDs: string[]`
- Make `getWidgetRenderState` stateless (derives feedIDs from results)
- Simplify `dispose()` to only call `unmountFn()`
- Extract shared test helpers to `test/createFeedsTestHelpers.ts`

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(instantsearch): fix lint errors in feeds test helpers

- Use `import type` for type-only imports
- Fix import ordering in test files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* review: use setState

* feedback: chain state when removing widgets (same as in dispose below)

* feedback: register FeedsWidgetDescription in IndexRenderState

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(instantsearch): add feeds widget for vanilla JS

Add `feeds()` widget that creates per-feed DOM containers and manages
FeedContainer lifecycle. Each feed gets a scoped `<div class="ais-Feeds-feed">`
and its own FeedContainer registered with the parent index.

- DOM creation, reuse, reorder, and cleanup per feed
- Deferred removal with coalesced timer (matches connectDynamicWidgets pattern)
- Dispose merges active + pending containers for clean teardown
- Exported from both main and UMD entrypoints
- 11 unit tests + 1 integration test with real search lifecycle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(react-instantsearch): add Feeds component and useFeeds hook

Add `<Feeds>` component that creates per-feed `IndexContext.Provider` scopes,
and `useFeeds` hook wrapping `connectFeeds` via `useConnector`.

- FeedContainer creation + registration fused in render body for SSR compat
- Deferred removal with coalesced timer (single ref pattern from useWidget)
- Unmount cleanup merges active + pending containers
- 5 unit tests + 2 integration tests (real InstantSearch + SSR)
- Exported from react-instantsearch-core

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(vue-instantsearch): add AisFeeds component

Add `<ais-feeds>` component with per-feed `AisFeedProvider` scoping via
Vue's provide/inject for `$_ais_getParentIndex`.

- Container reconciliation in watcher (side-effect-free render)
- Deferred removal with coalesced timer (single timer pattern)
- Unmount cleanup merges active + pending containers
- 5 unit tests + 1 integration test with real InstantSearch
- Exported as AisFeeds from vue-instantsearch
- Updated mock and index tests for compositionID support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: fix import ordering lint errors across Layer 3 files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(instantsearch): pass renderOptions to feedContainer.render() for type safety

FeedContainer.render() ignores the argument internally but IndexWidget type
requires it. Pass renderOptionsRef to satisfy TypeScript.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* temp: add examples

* fix: exclude composition examples from v4 type-check

@algolia/composition doesn't exist under algoliasearch v4.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(react-instantsearch-core): rename Feeds children render-prop to renderFeed

Switch from a children render function to a dedicated `renderFeed` prop with
an object argument `({ feedID })`. Keeps the API consistent with other React
InstantSearch components (`hitComponent`, `itemComponent`, `bannerComponent`),
which all use explicit, non-children customization props.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(vue-instantsearch): use indexOf instead of findIndex in mock removeWidgets

findIndex expects a predicate function, not the widget object — the call
threw at runtime the first time removeWidgets was invoked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: copilot feedback

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Haroen Viaene <hello@haroen.me>
Co-authored-by: Dan Rodriguez <daniel.rodriguez@algolia.com>
Co-authored-by: Dan Rodriguez <drodriguln@icloud.com>
Haroenv added a commit that referenced this pull request May 4, 2026
* feat(helper): support multifeed composition responses (#6954)

* feat(helper): support multifeed composition responses

Compositions can return multiple result sets (feeds), each identified by
a `feedID`. Previously, `_runComposition` spliced only 1 result per
derived helper, discarding additional feeds. This changes `queriesCount`
to `Infinity` so all feeds are captured, and builds a `_feedResults` map
and `_feedOrder` array on the primary `SearchResults` for downstream
consumption by `connectFeeds` (Layer 2).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: bump bundlesize limits for multifeed helper changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(helper): omit queriesCount for composition instead of using Infinity

Use undefined queriesCount to signal "take all results" in _dispatchAlgoliaResponse,
avoiding the Infinity hack. When queriesCount is undefined (composition path), use
results directly; when defined (regular search path), splice as before.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(helper): remove composition type definitions, defer to layer 2

The feedID, _feedResults, and _feedOrder type definitions have no consumers
yet. Defer the typing decision (interface vs class, naming) to Layer 2 when
connectFeeds introduces the first consumer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(helper): use lastResults.feeds array instead of _feedResults map

Replace _feedResults (Record<string, SearchResults>) and _feedOrder
(string[]) with a single lastResults.feeds (CompositionSearchResults[])
ordered array. This simplifies the API and ensures feeds survive the
SSR getInitialResults → JSON → hydration round-trip naturally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(helper): avoid circular ref in multifeed lastResults

Create a separate SearchResults instance for lastResults instead of
reusing feeds[0], which caused circular references during
JSON.stringify (lastResults.feeds[0] === lastResults).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* apply suggestions from code review

Co-authored-by: Haroen Viaene <hello@haroen.me>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Haroen Viaene <hello@haroen.me>

* feat(instantsearch): add connectFeeds connector for multifeed compositions (#6964)

* feat(helper): support multifeed composition responses

Compositions can return multiple result sets (feeds), each identified by
a `feedID`. Previously, `_runComposition` spliced only 1 result per
derived helper, discarding additional feeds. This changes `queriesCount`
to `Infinity` so all feeds are captured, and builds a `_feedResults` map
and `_feedOrder` array on the primary `SearchResults` for downstream
consumption by `connectFeeds` (Layer 2).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: bump bundlesize limits for multifeed helper changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(helper): omit queriesCount for composition instead of using Infinity

Use undefined queriesCount to signal "take all results" in _dispatchAlgoliaResponse,
avoiding the Infinity hack. When queriesCount is undefined (composition path), use
results directly; when defined (regular search path), splice as before.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(helper): remove composition type definitions, defer to layer 2

The feedID, _feedResults, and _feedOrder type definitions have no consumers
yet. Defer the typing decision (interface vs class, naming) to Layer 2 when
connectFeeds introduces the first consumer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(helper): use lastResults.feeds array instead of _feedResults map

Replace _feedResults (Record<string, SearchResults>) and _feedOrder
(string[]) with a single lastResults.feeds (CompositionSearchResults[])
ordered array. This simplifies the API and ensures feeds survive the
SSR getInitialResults → JSON → hydration round-trip naturally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(helper): avoid circular ref in multifeed lastResults

Create a separate SearchResults instance for lastResults instead of
reusing feeds[0], which caused circular references during
JSON.stringify (lastResults.feeds[0] === lastResults).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* apply suggestions from code review

Co-authored-by: Haroen Viaene <hello@haroen.me>

* refactor(instantsearch): extract storeRenderState to shared util

Move storeRenderState from module-private in index.ts to render-args.ts
so FeedContainer can reuse it in the feeds connector (Layer 2).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(instantsearch): centralize index widget types, widen for feedContainer

Introduce indexWidgetTypes const and IndexWidgetType as single source of
truth for index-like widget types. Update isIndexWidget, IndexWidgetDescription,
BuiltinTypes, and BuiltinWidgetTypes to use them. Add 'ais.feeds' and
'ais.feedContainer' to builtin type unions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(metadata): use isIndexWidget to recurse into feed containers

The metadata middleware hardcoded 'ais.index' instead of using
isIndexWidget, so widgets inside feed containers were invisible
to the Algolia Crawler metadata.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(instantsearch): add FeedContainer for per-feed widget subtrees

Made-with: Cursor

* feat(instantsearch): add connectFeeds connector for multifeed compositions

Made-with: Cursor

* test(instantsearch): add unit tests for FeedContainer and connectFeeds

Made-with: Cursor

* chore: bump bundlesize limits for multifeed connector

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(instantsearch): prevent double-init in FeedContainer with initialized flag

Replace `instantSearchInstance.started` check with a local `initialized` flag
so that child widgets added before `container.init()` are not eagerly
initialized. This prevents double-init when the parent index tree calls
`init()` on the container after widgets have already been added.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(instantsearch): simplify connectFeeds to stateless feedIDs computation

Remove FeedContainer lifecycle management from the connector — widget layers
now own container creation, registration, and cleanup. The connector only
computes feedIDs from results.feeds and exposes them as render state.

- Remove `widgets` param and internal `feedContainers` map
- Change render state from `feeds: [{ feedID, container }]` to `feedIDs: string[]`
- Make `getWidgetRenderState` stateless (derives feedIDs from results)
- Simplify `dispose()` to only call `unmountFn()`
- Extract shared test helpers to `test/createFeedsTestHelpers.ts`

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(instantsearch): fix lint errors in feeds test helpers

- Use `import type` for type-only imports
- Fix import ordering in test files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* review: use setState

* feedback: chain state when removing widgets (same as in dispose below)

* feedback: register FeedsWidgetDescription in IndexRenderState

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Haroen Viaene <hello@haroen.me>
Co-authored-by: Dan Rodriguez <drodriguln@icloud.com>

* feat: add feeds widgets for JS, React, and Vue (#6969)

* feat(helper): support multifeed composition responses

Compositions can return multiple result sets (feeds), each identified by
a `feedID`. Previously, `_runComposition` spliced only 1 result per
derived helper, discarding additional feeds. This changes `queriesCount`
to `Infinity` so all feeds are captured, and builds a `_feedResults` map
and `_feedOrder` array on the primary `SearchResults` for downstream
consumption by `connectFeeds` (Layer 2).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: bump bundlesize limits for multifeed helper changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(helper): omit queriesCount for composition instead of using Infinity

Use undefined queriesCount to signal "take all results" in _dispatchAlgoliaResponse,
avoiding the Infinity hack. When queriesCount is undefined (composition path), use
results directly; when defined (regular search path), splice as before.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(helper): remove composition type definitions, defer to layer 2

The feedID, _feedResults, and _feedOrder type definitions have no consumers
yet. Defer the typing decision (interface vs class, naming) to Layer 2 when
connectFeeds introduces the first consumer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(helper): use lastResults.feeds array instead of _feedResults map

Replace _feedResults (Record<string, SearchResults>) and _feedOrder
(string[]) with a single lastResults.feeds (CompositionSearchResults[])
ordered array. This simplifies the API and ensures feeds survive the
SSR getInitialResults → JSON → hydration round-trip naturally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(helper): avoid circular ref in multifeed lastResults

Create a separate SearchResults instance for lastResults instead of
reusing feeds[0], which caused circular references during
JSON.stringify (lastResults.feeds[0] === lastResults).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* apply suggestions from code review

Co-authored-by: Haroen Viaene <hello@haroen.me>

* refactor(instantsearch): extract storeRenderState to shared util

Move storeRenderState from module-private in index.ts to render-args.ts
so FeedContainer can reuse it in the feeds connector (Layer 2).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(instantsearch): centralize index widget types, widen for feedContainer

Introduce indexWidgetTypes const and IndexWidgetType as single source of
truth for index-like widget types. Update isIndexWidget, IndexWidgetDescription,
BuiltinTypes, and BuiltinWidgetTypes to use them. Add 'ais.feeds' and
'ais.feedContainer' to builtin type unions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(metadata): use isIndexWidget to recurse into feed containers

The metadata middleware hardcoded 'ais.index' instead of using
isIndexWidget, so widgets inside feed containers were invisible
to the Algolia Crawler metadata.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(instantsearch): add FeedContainer for per-feed widget subtrees

Made-with: Cursor

* feat(instantsearch): add connectFeeds connector for multifeed compositions

Made-with: Cursor

* test(instantsearch): add unit tests for FeedContainer and connectFeeds

Made-with: Cursor

* chore: bump bundlesize limits for multifeed connector

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(instantsearch): prevent double-init in FeedContainer with initialized flag

Replace `instantSearchInstance.started` check with a local `initialized` flag
so that child widgets added before `container.init()` are not eagerly
initialized. This prevents double-init when the parent index tree calls
`init()` on the container after widgets have already been added.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(instantsearch): simplify connectFeeds to stateless feedIDs computation

Remove FeedContainer lifecycle management from the connector — widget layers
now own container creation, registration, and cleanup. The connector only
computes feedIDs from results.feeds and exposes them as render state.

- Remove `widgets` param and internal `feedContainers` map
- Change render state from `feeds: [{ feedID, container }]` to `feedIDs: string[]`
- Make `getWidgetRenderState` stateless (derives feedIDs from results)
- Simplify `dispose()` to only call `unmountFn()`
- Extract shared test helpers to `test/createFeedsTestHelpers.ts`

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(instantsearch): fix lint errors in feeds test helpers

- Use `import type` for type-only imports
- Fix import ordering in test files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* review: use setState

* feedback: chain state when removing widgets (same as in dispose below)

* feedback: register FeedsWidgetDescription in IndexRenderState

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(instantsearch): add feeds widget for vanilla JS

Add `feeds()` widget that creates per-feed DOM containers and manages
FeedContainer lifecycle. Each feed gets a scoped `<div class="ais-Feeds-feed">`
and its own FeedContainer registered with the parent index.

- DOM creation, reuse, reorder, and cleanup per feed
- Deferred removal with coalesced timer (matches connectDynamicWidgets pattern)
- Dispose merges active + pending containers for clean teardown
- Exported from both main and UMD entrypoints
- 11 unit tests + 1 integration test with real search lifecycle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(react-instantsearch): add Feeds component and useFeeds hook

Add `<Feeds>` component that creates per-feed `IndexContext.Provider` scopes,
and `useFeeds` hook wrapping `connectFeeds` via `useConnector`.

- FeedContainer creation + registration fused in render body for SSR compat
- Deferred removal with coalesced timer (single ref pattern from useWidget)
- Unmount cleanup merges active + pending containers
- 5 unit tests + 2 integration tests (real InstantSearch + SSR)
- Exported from react-instantsearch-core

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(vue-instantsearch): add AisFeeds component

Add `<ais-feeds>` component with per-feed `AisFeedProvider` scoping via
Vue's provide/inject for `$_ais_getParentIndex`.

- Container reconciliation in watcher (side-effect-free render)
- Deferred removal with coalesced timer (single timer pattern)
- Unmount cleanup merges active + pending containers
- 5 unit tests + 1 integration test with real InstantSearch
- Exported as AisFeeds from vue-instantsearch
- Updated mock and index tests for compositionID support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: fix import ordering lint errors across Layer 3 files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(instantsearch): pass renderOptions to feedContainer.render() for type safety

FeedContainer.render() ignores the argument internally but IndexWidget type
requires it. Pass renderOptionsRef to satisfy TypeScript.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* temp: add examples

* fix: exclude composition examples from v4 type-check

@algolia/composition doesn't exist under algoliasearch v4.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(react-instantsearch-core): rename Feeds children render-prop to renderFeed

Switch from a children render function to a dedicated `renderFeed` prop with
an object argument `({ feedID })`. Keeps the API consistent with other React
InstantSearch components (`hitComponent`, `itemComponent`, `bannerComponent`),
which all use explicit, non-children customization props.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(vue-instantsearch): use indexOf instead of findIndex in mock removeWidgets

findIndex expects a predicate function, not the widget object — the call
threw at runtime the first time removeWidgets was invoked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: copilot feedback

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Haroen Viaene <hello@haroen.me>
Co-authored-by: Dan Rodriguez <daniel.rodriguez@algolia.com>
Co-authored-by: Dan Rodriguez <drodriguln@icloud.com>

* feat(instantsearch): SSR support for composition multifeed (#6975)

* feat(helper): support multifeed composition responses

Compositions can return multiple result sets (feeds), each identified by
a `feedID`. Previously, `_runComposition` spliced only 1 result per
derived helper, discarding additional feeds. This changes `queriesCount`
to `Infinity` so all feeds are captured, and builds a `_feedResults` map
and `_feedOrder` array on the primary `SearchResults` for downstream
consumption by `connectFeeds` (Layer 2).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: bump bundlesize limits for multifeed helper changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(helper): omit queriesCount for composition instead of using Infinity

Use undefined queriesCount to signal "take all results" in _dispatchAlgoliaResponse,
avoiding the Infinity hack. When queriesCount is undefined (composition path), use
results directly; when defined (regular search path), splice as before.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(helper): remove composition type definitions, defer to layer 2

The feedID, _feedResults, and _feedOrder type definitions have no consumers
yet. Defer the typing decision (interface vs class, naming) to Layer 2 when
connectFeeds introduces the first consumer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(helper): use lastResults.feeds array instead of _feedResults map

Replace _feedResults (Record<string, SearchResults>) and _feedOrder
(string[]) with a single lastResults.feeds (CompositionSearchResults[])
ordered array. This simplifies the API and ensures feeds survive the
SSR getInitialResults → JSON → hydration round-trip naturally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(helper): avoid circular ref in multifeed lastResults

Create a separate SearchResults instance for lastResults instead of
reusing feeds[0], which caused circular references during
JSON.stringify (lastResults.feeds[0] === lastResults).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* apply suggestions from code review

Co-authored-by: Haroen Viaene <hello@haroen.me>

* refactor(instantsearch): extract storeRenderState to shared util

Move storeRenderState from module-private in index.ts to render-args.ts
so FeedContainer can reuse it in the feeds connector (Layer 2).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(instantsearch): centralize index widget types, widen for feedContainer

Introduce indexWidgetTypes const and IndexWidgetType as single source of
truth for index-like widget types. Update isIndexWidget, IndexWidgetDescription,
BuiltinTypes, and BuiltinWidgetTypes to use them. Add 'ais.feeds' and
'ais.feedContainer' to builtin type unions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(metadata): use isIndexWidget to recurse into feed containers

The metadata middleware hardcoded 'ais.index' instead of using
isIndexWidget, so widgets inside feed containers were invisible
to the Algolia Crawler metadata.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(instantsearch): add FeedContainer for per-feed widget subtrees

Made-with: Cursor

* feat(instantsearch): add connectFeeds connector for multifeed compositions

Made-with: Cursor

* test(instantsearch): add unit tests for FeedContainer and connectFeeds

Made-with: Cursor

* chore: bump bundlesize limits for multifeed connector

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(instantsearch): prevent double-init in FeedContainer with initialized flag

Replace `instantSearchInstance.started` check with a local `initialized` flag
so that child widgets added before `container.init()` are not eagerly
initialized. This prevents double-init when the parent index tree calls
`init()` on the container after widgets have already been added.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(instantsearch): simplify connectFeeds to stateless feedIDs computation

Remove FeedContainer lifecycle management from the connector — widget layers
now own container creation, registration, and cleanup. The connector only
computes feedIDs from results.feeds and exposes them as render state.

- Remove `widgets` param and internal `feedContainers` map
- Change render state from `feeds: [{ feedID, container }]` to `feedIDs: string[]`
- Make `getWidgetRenderState` stateless (derives feedIDs from results)
- Simplify `dispose()` to only call `unmountFn()`
- Extract shared test helpers to `test/createFeedsTestHelpers.ts`

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(instantsearch): fix lint errors in feeds test helpers

- Use `import type` for type-only imports
- Fix import ordering in test files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* review: use setState

* feedback: chain state when removing widgets (same as in dispose below)

* feedback: register FeedsWidgetDescription in IndexRenderState

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(instantsearch): add feeds widget for vanilla JS

Add `feeds()` widget that creates per-feed DOM containers and manages
FeedContainer lifecycle. Each feed gets a scoped `<div class="ais-Feeds-feed">`
and its own FeedContainer registered with the parent index.

- DOM creation, reuse, reorder, and cleanup per feed
- Deferred removal with coalesced timer (matches connectDynamicWidgets pattern)
- Dispose merges active + pending containers for clean teardown
- Exported from both main and UMD entrypoints
- 11 unit tests + 1 integration test with real search lifecycle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(react-instantsearch): add Feeds component and useFeeds hook

Add `<Feeds>` component that creates per-feed `IndexContext.Provider` scopes,
and `useFeeds` hook wrapping `connectFeeds` via `useConnector`.

- FeedContainer creation + registration fused in render body for SSR compat
- Deferred removal with coalesced timer (single ref pattern from useWidget)
- Unmount cleanup merges active + pending containers
- 5 unit tests + 2 integration tests (real InstantSearch + SSR)
- Exported from react-instantsearch-core

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(vue-instantsearch): add AisFeeds component

Add `<ais-feeds>` component with per-feed `AisFeedProvider` scoping via
Vue's provide/inject for `$_ais_getParentIndex`.

- Container reconciliation in watcher (side-effect-free render)
- Deferred removal with coalesced timer (single timer pattern)
- Unmount cleanup merges active + pending containers
- 5 unit tests + 1 integration test with real InstantSearch
- Exported as AisFeeds from vue-instantsearch
- Updated mock and index tests for compositionID support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: fix import ordering lint errors across Layer 3 files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(instantsearch): pass renderOptions to feedContainer.render() for type safety

FeedContainer.render() ignores the argument internally but IndexWidget type
requires it. Pass renderOptionsRef to satisfy TypeScript.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* temp: add examples

* fix: exclude composition examples from v4 type-check

@algolia/composition doesn't exist under algoliasearch v4.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(react-instantsearch-core): rename Feeds children render-prop to renderFeed

Switch from a children render function to a dedicated `renderFeed` prop with
an object argument `({ feedID })`. Keeps the API consistent with other React
InstantSearch components (`hitComponent`, `itemComponent`, `bannerComponent`),
which all use explicit, non-children customization props.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(vue-instantsearch): use indexOf instead of findIndex in mock removeWidgets

findIndex expects a predicate function, not the widget object — the call
threw at runtime the first time removeWidgets was invoked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(instantsearch): extract isTwoPassWidget utility

Replace inline `$$type === 'ais.dynamicWidgets'` checks with a shared
`isTwoPassWidget` predicate. This prepares for the feeds widget which
also requires a two-pass SSR cycle, and fixes a missing `shouldRefetch ||`
in InitializePromise.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(instantsearch): add SSR support for composition multifeed

Serialize per-feed results as `compositionFeedsResults` in
`getInitialResults`, hydrate them in `hydrateSearchClient`, and
reconstruct `lastResults.feeds` in the feeds connector so that
composition multifeed works with server-side rendering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(instantsearch): add composition multifeed SSR integration test

End-to-end integration test covering the full SSR cycle: server-side
rendering with getInitialResults, hydration via hydrateSearchClient,
and client-side rehydration of per-feed results.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: add Next.js App Router composition example

Demo app showing composition multifeed with SSR using
react-instantsearch-nextjs. Excluded from v4 type-check
since it uses composition-only APIs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(react-instantsearch-core): adapt Feeds usages to renderFeed prop

Update the SSR test and the Next.js App Router composition example to the
new `renderFeed` prop introduced on Layer 3.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: bump bundlesize limit for SSR multifeed

The composition multifeed SSR additions push the development bundle just
over the 256.5 kB ceiling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Haroen Viaene <hello@haroen.me>
Co-authored-by: Dan Rodriguez <drodriguln@icloud.com>

* chore: remove internal composition examples (#6977)

* feat(helper): support multifeed composition responses

Compositions can return multiple result sets (feeds), each identified by
a `feedID`. Previously, `_runComposition` spliced only 1 result per
derived helper, discarding additional feeds. This changes `queriesCount`
to `Infinity` so all feeds are captured, and builds a `_feedResults` map
and `_feedOrder` array on the primary `SearchResults` for downstream
consumption by `connectFeeds` (Layer 2).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: bump bundlesize limits for multifeed helper changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(helper): omit queriesCount for composition instead of using Infinity

Use undefined queriesCount to signal "take all results" in _dispatchAlgoliaResponse,
avoiding the Infinity hack. When queriesCount is undefined (composition path), use
results directly; when defined (regular search path), splice as before.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(helper): remove composition type definitions, defer to layer 2

The feedID, _feedResults, and _feedOrder type definitions have no consumers
yet. Defer the typing decision (interface vs class, naming) to Layer 2 when
connectFeeds introduces the first consumer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(helper): use lastResults.feeds array instead of _feedResults map

Replace _feedResults (Record<string, SearchResults>) and _feedOrder
(string[]) with a single lastResults.feeds (CompositionSearchResults[])
ordered array. This simplifies the API and ensures feeds survive the
SSR getInitialResults → JSON → hydration round-trip naturally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(helper): avoid circular ref in multifeed lastResults

Create a separate SearchResults instance for lastResults instead of
reusing feeds[0], which caused circular references during
JSON.stringify (lastResults.feeds[0] === lastResults).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* apply suggestions from code review

Co-authored-by: Haroen Viaene <hello@haroen.me>

* refactor(instantsearch): extract storeRenderState to shared util

Move storeRenderState from module-private in index.ts to render-args.ts
so FeedContainer can reuse it in the feeds connector (Layer 2).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(instantsearch): centralize index widget types, widen for feedContainer

Introduce indexWidgetTypes const and IndexWidgetType as single source of
truth for index-like widget types. Update isIndexWidget, IndexWidgetDescription,
BuiltinTypes, and BuiltinWidgetTypes to use them. Add 'ais.feeds' and
'ais.feedContainer' to builtin type unions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(metadata): use isIndexWidget to recurse into feed containers

The metadata middleware hardcoded 'ais.index' instead of using
isIndexWidget, so widgets inside feed containers were invisible
to the Algolia Crawler metadata.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(instantsearch): add FeedContainer for per-feed widget subtrees

Made-with: Cursor

* feat(instantsearch): add connectFeeds connector for multifeed compositions

Made-with: Cursor

* test(instantsearch): add unit tests for FeedContainer and connectFeeds

Made-with: Cursor

* chore: bump bundlesize limits for multifeed connector

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(instantsearch): prevent double-init in FeedContainer with initialized flag

Replace `instantSearchInstance.started` check with a local `initialized` flag
so that child widgets added before `container.init()` are not eagerly
initialized. This prevents double-init when the parent index tree calls
`init()` on the container after widgets have already been added.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(instantsearch): simplify connectFeeds to stateless feedIDs computation

Remove FeedContainer lifecycle management from the connector — widget layers
now own container creation, registration, and cleanup. The connector only
computes feedIDs from results.feeds and exposes them as render state.

- Remove `widgets` param and internal `feedContainers` map
- Change render state from `feeds: [{ feedID, container }]` to `feedIDs: string[]`
- Make `getWidgetRenderState` stateless (derives feedIDs from results)
- Simplify `dispose()` to only call `unmountFn()`
- Extract shared test helpers to `test/createFeedsTestHelpers.ts`

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(instantsearch): fix lint errors in feeds test helpers

- Use `import type` for type-only imports
- Fix import ordering in test files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* review: use setState

* feedback: chain state when removing widgets (same as in dispose below)

* feedback: register FeedsWidgetDescription in IndexRenderState

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(instantsearch): add feeds widget for vanilla JS

Add `feeds()` widget that creates per-feed DOM containers and manages
FeedContainer lifecycle. Each feed gets a scoped `<div class="ais-Feeds-feed">`
and its own FeedContainer registered with the parent index.

- DOM creation, reuse, reorder, and cleanup per feed
- Deferred removal with coalesced timer (matches connectDynamicWidgets pattern)
- Dispose merges active + pending containers for clean teardown
- Exported from both main and UMD entrypoints
- 11 unit tests + 1 integration test with real search lifecycle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(react-instantsearch): add Feeds component and useFeeds hook

Add `<Feeds>` component that creates per-feed `IndexContext.Provider` scopes,
and `useFeeds` hook wrapping `connectFeeds` via `useConnector`.

- FeedContainer creation + registration fused in render body for SSR compat
- Deferred removal with coalesced timer (single ref pattern from useWidget)
- Unmount cleanup merges active + pending containers
- 5 unit tests + 2 integration tests (real InstantSearch + SSR)
- Exported from react-instantsearch-core

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(vue-instantsearch): add AisFeeds component

Add `<ais-feeds>` component with per-feed `AisFeedProvider` scoping via
Vue's provide/inject for `$_ais_getParentIndex`.

- Container reconciliation in watcher (side-effect-free render)
- Deferred removal with coalesced timer (single timer pattern)
- Unmount cleanup merges active + pending containers
- 5 unit tests + 1 integration test with real InstantSearch
- Exported as AisFeeds from vue-instantsearch
- Updated mock and index tests for compositionID support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: fix import ordering lint errors across Layer 3 files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(instantsearch): pass renderOptions to feedContainer.render() for type safety

FeedContainer.render() ignores the argument internally but IndexWidget type
requires it. Pass renderOptionsRef to satisfy TypeScript.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* temp: add examples

* fix: exclude composition examples from v4 type-check

@algolia/composition doesn't exist under algoliasearch v4.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(react-instantsearch-core): rename Feeds children render-prop to renderFeed

Switch from a children render function to a dedicated `renderFeed` prop with
an object argument `({ feedID })`. Keeps the API consistent with other React
InstantSearch components (`hitComponent`, `itemComponent`, `bannerComponent`),
which all use explicit, non-children customization props.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(vue-instantsearch): use indexOf instead of findIndex in mock removeWidgets

findIndex expects a predicate function, not the widget object — the call
threw at runtime the first time removeWidgets was invoked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(instantsearch): extract isTwoPassWidget utility

Replace inline `$$type === 'ais.dynamicWidgets'` checks with a shared
`isTwoPassWidget` predicate. This prepares for the feeds widget which
also requires a two-pass SSR cycle, and fixes a missing `shouldRefetch ||`
in InitializePromise.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(instantsearch): add SSR support for composition multifeed

Serialize per-feed results as `compositionFeedsResults` in
`getInitialResults`, hydrate them in `hydrateSearchClient`, and
reconstruct `lastResults.feeds` in the feeds connector so that
composition multifeed works with server-side rendering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(instantsearch): add composition multifeed SSR integration test

End-to-end integration test covering the full SSR cycle: server-side
rendering with getInitialResults, hydration via hydrateSearchClient,
and client-side rehydration of per-feed results.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: add Next.js App Router composition example

Demo app showing composition multifeed with SSR using
react-instantsearch-nextjs. Excluded from v4 type-check
since it uses composition-only APIs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(react-instantsearch-core): adapt Feeds usages to renderFeed prop

Update the SSR test and the Next.js App Router composition example to the
new `renderFeed` prop introduced on Layer 3.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: bump bundlesize limit for SSR multifeed

The composition multifeed SSR additions push the development bundle just
over the 256.5 kB ceiling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: remove internal composition examples

These examples reference an internal Algolia application and should not
be published.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reapply removal commits after merge commit

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Haroen Viaene <hello@haroen.me>
Co-authored-by: Dan Rodriguez <drodriguln@icloud.com>
Co-authored-by: Dan Rodriguez <daniel.rodriguez@algolia.com>

* chore: bump bundlesize

* fix: stale umd import

* chore: remove lock file deps from removed examples

---------

Co-authored-by: Emmanuel Krebs <e-krebs@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Haroen Viaene <hello@haroen.me>
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.

5 participants