Guides / Building Search UI / Widgets

You are currently reading the documentation for InstantSearch.js V4. Read our migration guide to learn how to upgrade from V3 to V4. You can still access the V3 documentation for this page.

InstantSearch.js comes with many widgets, by default. If those widgets are constraining, each rendering can change by using connectors.

You might find yourself in a situation where both widgets and connectors don’t cover your use case, that’s why it’s possible to create your own widget. Making widgets is the most advanced way of customizing your search experience and it requires a deeper knowledge of InstantSearch.js and Algolia.

You are trying to create your own widget with InstantSearch.js and that’s awesome. That also means that you couldn’t find the widgets or built-in options you were looking for. We’d love to hear about your use case as the mission of the InstantSearch libraries is to offer the best out-of-the-box experience. Don’t hesitate to send a quick message explaining what you were trying to achieve either using the form at the end of that page or directly by submitting a feature request.

To be able to start making your own widgets, there are some elements that you need to know:

  • the widget lifecycle
  • how to interact with the search state

Quick start

You can use create-instantsearch-app. Similarly to other interactive command-line applications, you can run it either with npm or with yarn:

1
2
3
npx create-instantsearch-app --template 'InstantSearch.js widget' 'my-widget'
# or
yarn create instantsearch-app --template 'InstantSearch.js widget' 'my-widget'

After this has run, you have access to an empty custom widget, focused on TypeScript usage. TypeScript is recommended in a custom widget, as it helps guide towards correct implementations.

If you aren’t comfortable with TypeScript, you can remove all types, and create a plain JavaScript implementation.

Types

A widget exposes all its behavior in the WidgetDescription in the src/types.ts file. The following keys are present:

  1. $$type: an identifier for this widget. Other widgets or tools can read this.
  2. renderState: the shape of the returned value of getWidgetRenderState, the data required to render the component, like items, refine function, etc.
  3. indexRenderState: the shape of the returned value of getWidgetRenderState, which is the same shape as renderState, but scoped for the whole application. If you can have multiple of the same type widget with different attribute, scoped per attribute as well.
  4. indexUiState: the shape of the returned value of getWidgetUiState, a translation of this widget’s search parameters into an object. This is also scoped per attribute if your widget accepts an attribute.

Further there’s also the connectorParams, parameters used for changes in logic, and widgetParams, parameters for UI and rendering.

Connector

InstantSearch.js defines the widget render lifecycle methods of the widgets. Each of these methods is optional, but either an init or render step must be present. If your widget would not require those life cycles, you might be looking for a middleware instead.

Connectors are a function which take a rendering implementation, connectorParams, and return a widget object:

1
const connectMyWidget = (renderFn, unmountFn) => connectorParams => widget;

Inside the implementation of widget, you call renderFn in init and render with the result of getWidgetRenderState and call unmountFn in the dispose hook.

Providing render parameters

Basic widgets are plain JS objects with the following methods:

  • init, sets up the widget. Called before the first search.
  • render, updates the widget with the new information from search results. Called each time results come back from Algolia
  • dispose, removes the configuration specified in the getWidgetSearchParameters method. Called when you remove the widget or when InstantSearch disposes itself. Used to remove search parameters set by this widget.

To simplify the init and render stages, as well as for accessing the rendered parameters outside of the widget, for example via panel. This is handled by the following methods:

  • getWidgetRenderState, retrieve the rendered parameters for this widget, like items, refine, widgetParams, etc.
  • getRenderState, retrieve the global rendered parameters, calling its own getWidgetRenderState and returning it in a nested object by widget name and possibly attribute.

Interaction with routing

Between those steps the uiState gets calculated by the widget. These methods are required for the widget to participate in routing. This is handled by the following methods:

  • getWidgetUiState, transforms search parameters into UI State.
  • getWidgetSearchParameters, transforms UI State back into search parameters.

Example of a connector

Translating this to code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
const connectMyWidget: MyWidgetConnector =
  (renderFn, unmountFn = () => {}) => (widgetParams) => ({
    $$type: 'my-organisation.my-widget',

    init(initOptions) {
      // initOptions contains three keys:
      //   - helper: to modify the search state and propagate the user interaction
      //   - state: which is the state of the search at this point
      //   - templatesConfig: the configuration of the templates
      const { instantSearchInstance } = initOptions;

      renderFn(
        {
          ...this.getWidgetRenderState(initOptions),
          instantSearchInstance,
        },
        true
      );
    },

    render(renderOptions) {
      // renderOptions contains four keys:
      //   - results: the results from the last request
      //   - helper: to modify the search state and propagate the user interaction
      //   - state: the state at this point
      //   - createURL: function to create a url for ui state
      const { instantSearchInstance } = renderOptions;

      renderFn(
        {
          ...this.getWidgetRenderState(renderOptions),
          instantSearchInstance,
        },
        false
      );
    },

    dispose(disposeOptions) {
      // disposeOptions contains one key:
      //   - state: the state at this point to
      //
      // The dispose method should return the next state of the search,
      // if it has been modified.
      unmountFn();
    },

    getWidgetUiState(uiState, widgetStateOptions) {
      // widgetStateOptions contains two keys:
      //   - searchParameters: to compute the next uiState
      //   - helper: to get information about the state of the search
      //
      // The function must return the next uiState
      return {
        ...uiState,
        myWidget: {
          // ...
        }
      };
    },

    getWidgetSearchParameters(searchParameters, widgetSearchParametersOptions) {
      // widgetSearchParametersOptions contains one key:
      //   - uiState: to compute the next SearchParameters
      //
      // The function must return the next SearchParameters
      return searchParameters;
    },

    getRenderState(renderState, renderOptions) {
      // renderOptions contains four keys:
      //   - results: the results from the last request
      //   - helper: to modify the search state and propagate the user interaction
      //   - state: the state at this point
      //   - createURL: function to create a url for ui state

      const widgetRenderState = this.getWidgetRenderState(renderOptions)
      return {
        ...renderState,
        myWidget: {
          // widgetRenderState
        }
      };
    },

    getWidgetRenderState(renderOptions) {
      // renderOptions contains four keys:
      //   - results: the results from the last request
      //   - helper: to modify the search state and propagate the user interaction
      //   - state: the state at this point
      //   - createURL: function to create a url for ui state
      if (!renderOptions.results) {
        // default rendering state without results
        return {
          widgetParams,
        };
      }

      // rendering state when there are results
      return {
        widgetParams,
      };
    },
  });

Rendering

Rendering has two parts: a rendering function and a unmounting function. Both of those get created in a factory to give access to the same DOM container. In code that looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/*
 * Creates the render and dispose functions
 * This function is called once by the connector when the widget is created and is returning
 *  - the `render` function used to render the widget
 *  - the `dispose` function used to clean the changes made by the widget
 * It can also be used to keep references of objects that must be reused between renders
 */
export const createMyWidgetRenderer: MyWidgetRendererCreator = ({
  container,
}) => {
  const containerNode: Element =
    typeof container === 'string'
      ? document.querySelector(container)!
      : container;

  const root = document.createElement('div');

  return {
    /*
     * The render function passed to the connector
     * This function is called when we need to render the widget.
     * The render appends when:
     * - the widget is added to InstantSearch
     * - we receive new results from Algolia
     */
    render(renderOptions, isFirstRender) {
      /*
       * `renderOptions` contains all options passed by the connector to the renderer, it contains everything needed for the rendering of the component
       */

      if (isFirstRender) {
        /*
         * When the widget is rendered for the first time `isFirstRender` is set to `true`
         * This is when we will create everything that must be reused between renders (containers, event listeners, etc.)
         */
        containerNode.appendChild(root);
      }

      /*
       * Rendering
       */

    },
    /*
     * The dispose function passed to the connector
     * This function is called when the widget is removed from InstantSearch.
     * It must be used to remove any changes made by the render function (DOM changes, global event listeners, etc.)
     */
    dispose() {
      containerNode.removeChild(root);
    },
  };
};

Widget

A widget is a connector combined with rendering, ready to use. In code that looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export const myWidget: MyWidgetWidgetCreator = function myWidget(
  widgetParams
) {
  const rendererWidgetParams: MyWidgetWidgetParams = {
    container: widgetParams.container,
    // TODO: pick the widget-only parameters from the widgetParams
  };

  const { render, dispose } = createMyWidgetRenderer(
    rendererWidgetParams
  );

  const createWidget = connectMyWidget(render, dispose);

  const connectorParams: MyWidgetConnectorParams = {
    // TODO: pick the connector-only parameters from the widgetParams
  };

  return {
    ...createWidget(connectorParams),
    $$widgetType: 'my-organization.my-widget',
  };
};

Did you find this page helpful?