Getting started with InstantSearch.js in Angular
This guide shows you how to use InstantSearch.js directly within your Angular projects, focusing on state management.
This approach lets you build a custom search solution without relying on Angular InstantSearch (which has compatibility issues with the latest version of Angular). It streamlines the development process while enabling you to customize and optimize your search interfaces exactly how you need them.
Base use-case
Instead of needing a component to use InstantSearch, the instance can be stored in a service, with some utility methods forwarded to the instance. This allows for a single instance to be used across multiple components, and for the instance to be started in a single component.
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
import { Injectable } from '@angular/core';
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import InstantSearch from 'instantsearch.js/es/lib/InstantSearch';
import type { IndexWidget, Widget } from 'instantsearch.js';
const searchClient = algoliasearch(
'YourApplicationID',
'YourSearchOnlyAPIKey'
);
@Injectable({
providedIn: 'root',
})
export class InstantSearchService {
public instantSearchInstance: InstantSearch;
constructor(router: Router) {
this.instantSearchInstance = new InstantSearch({
searchClient,
indexName: 'instant_search',
future: { preserveSharedStateOnUnmount: true },
});
}
start() {
this.instantSearchInstance.start();
}
addWidgets(widgets: Array<IndexWidget | Widget>) {
this.instantSearchInstance.addWidgets(widgets);
}
removeWidgets(widgets: Array<IndexWidget | Widget>) {
this.instantSearchInstance.removeWidgets(widgets);
}
}
This service can be consumed by any component through Dependency Injection:
1
2
3
4
5
6
7
8
9
10
11
12
13
import { Component } from '@angular/core';
import { InstantSearchService } from '../instant-search.service';
@Component({
selector: 'app-search',
standalone: true,
templateUrl: './search.component.html',
})
export class SearchComponent {
ngAfterContentInit() {
this.InstantSearchService.start();
}
}
You can then add widgets in any component using the service, whether in the constructor or ngOnInit
, and the instance can be started in ngAfterContentInit
in a single component to start the instance.
Connectors can be used to track state changes and bind the data to the component:
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
import { Component } from '@angular/core';
import { InstantSearchService } from '../instant-search.service';
import { BaseHit } from 'instantsearch.js';
import { connectHits, connectSearchBox } from 'instantsearch.js/es/connectors';
@Component({
selector: 'app-search',
standalone: true,
templateUrl: './search.component.html',
})
export class SearchComponent {
public hits: BaseHit[] = [];
public refine: (query: string) => void;
public query: string;
constructor(private InstantSearchService: InstantSearchService) {
this.InstantSearchService.addWidgets([
connectSearchBox({ refine, query }) => {
this.refine = refine;
this.query = query;
})({
// ...widgetParameters
}),
connectHits(({ hits }) => {
this.hits = hits;
})({}),
]);
}
ngAfterContentInit() {
this.InstantSearchService.start();
}
public search(event: Event) {
this.refine!((event.target as HTMLInputElement).value);
}
}
This lets you have multiple widgets in a single component.
State can then be used in a template, which will re-render when new results are received:
1
2
3
4
5
6
<input (input)="search($event)" [value]="query" />
<ul>
@for (let hit of hits; track hit.objectID) {
<li>{{ hit.name }}</li>
}
</ul>
Server-side rendering and routing
With this service, routing works out of the box, by reading the Angular Router’s start state and applying it to the InstantSearch instance.
Server-side rendering also works without extra code, as Angular will render the page with the initial state coming from the network request.
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
import { Injectable } from '@angular/core';
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import history from 'instantsearch.js/es/lib/routers/history';
import InstantSearch from 'instantsearch.js/es/lib/InstantSearch';
import type { Widget } from 'instantsearch.js';
import { Router } from '@angular/router';
const searchClient = algoliasearch(
'YourApplicationID',
'YourSearchOnlyAPIKey'
);
@Injectable({
providedIn: 'root',
})
export class InstantSearchService {
public instantSearchInstance: InstantSearch;
constructor(router: Router) {
this.instantSearchInstance = new InstantSearch({
searchClient,
indexName: 'instant_search',
future: { preserveSharedStateOnUnmount: true },
routing: {
router: history({
getLocation: () => {
if (typeof window === 'undefined') {
// no other way to get this in constructor
return new URL(
router['location']._locationStrategy._platformLocation.href
) as unknown as Location;
}
return window.location;
},
}),
},
});
}
}
Multi-index
Multi-index is supported by index
in the component.
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
import { Component } from '@angular/core';
import { InstantSearchService } from '../instant-search.service';
import { BaseHit } from 'instantsearch.js';
import { connectHits } from 'instantsearch.js/es/connectors';
import index from 'instantsearch.js/es/widgets/index/index';
@Component({
selector: 'app-search',
standalone: true,
templateUrl: './search.component.html',
})
export class SearchComponent {
public hits: BaseHit[] = [];
constructor(private InstantSearchService: InstantSearchService) {
this.InstantSearchService.addWidgets([
index({ indexName: 'instant_search' }).addWidgets([
connectHits(({ hits }) => {
this.hits = hits;
})({}),
]),
]);
}
ngAfterContentInit() {
this.InstantSearchService.start();
}
}