Nuxt Swiftsearch
Composables

useAisWidget

Read connector render-state inside a custom Swiftsearch component.

useAisWidget(connectorName, widgetId?) returns the live render-state for a connector that the parent <AisInstantSearch> already mounted.

It powers every shipped widget component (<AisRefinementList>, <AisMenu>, etc.) and is the primary extension point when you build your own.

Signature

useAisWidget<TWidget extends keyof RenderState["string"]>(
  widgetName: TWidget,
  widgetId?: string,
): {
  instance: Ref<InstantSearch>;
  state: Ref<RenderState[string][TWidget]>;
  widgetParams: ComputedRef<unknown>;
};

When to pass widgetId

  • No widgetId — the composable returns the connector's bucket for the current <AisIndex> scope. Use this when there is at most one instance of the connector per index (<AisHits>, <AisStats>, single <AisRefinementList> per attribute, etc.).
  • widgetId set — the composable reads the state from a registry that belongs to the InstantSearch instance. The key is ${widgetName}-${indexScope}-${widgetId}. Each widget writes its state there on its first render, so a component can mount at any time, also after the first search. Use this when you mount the same connector more than once in the same index and each instance must keep separate state.

The id prop on the shipped components is forwarded straight to this argument.

How the state updates

useAisWidget does not watch the InstantSearch instance deeply. Until v1.3.2, each widget component ran watch(instance, …, { deep: true }). Vue then walked the full search instance (helper, results, all widgets) once per widget on each render. On a page with many widgets, this walk was the largest startup task (#59).

The composable now uses one of two update paths:

  • widgetId set — state is a computed that reads the widget's state ref from the registry. The connector writes a new render state into that ref on each render, and Vue sends the change straight to the component. No watcher on the instance is necessary.
  • No widgetId — state is set again from instance.renderState in a watchEffect. The module calls triggerRef on the instance after each render, and the effect runs again.
The deep watch also had a hidden role. triggerRef gives the watcher the same instance object, so a shallow watch callback does not run. The deep option forced the callback to run. That is why { deep: false } alone is not a safe replacement for widgets without an id. The watchEffect runs again on each trigger, with no comparison and no traversal.

Usage

components/MyHits.vue
<script setup lang="ts">
import { useAisWidget } from "#imports";

defineProps<{ id?: string }>();

const { state } = useAisWidget("hits", id);
</script>

<template>
  <ul v-if="state">
    <li v-for="hit in state.hits" :key="hit.objectID">
      {{ hit.name }}
    </li>
  </ul>
</template>

Throwing behavior

If no widget is registered for the requested connector at mount time, useAisWidget throws:

Connector for component <name> not found, did you forget to add the proper widget?

In manual mode it usually means the matching useAis*({...}, id) call is missing from the :widgets array. In declarative mode the known causes are all surfaced at build time:

  • the widget element was skipped by the transform (a [nuxt-swiftsearch] skipped <Tag> build warning names the element and the reason),
  • the widget lives in a child component and childWidgetDiscovery is off, the component could not be resolved to a file, or it is a server component (see widgets inside child components).
Copyright © 2026