Skip to content

Commit 5f8ba5d

Browse files
authoredAug 4, 2021
feat(ts): allow custom ui state and route state in routing (#4816)
* feat(ts): allow custom ui state and route state in routing While I couldn't find a way to make InstantSearch itself generic (this gets passed to many places, which then loses generic), using the routing middleware directly is possible like this now ```ts import instantsearch from 'instantsearch.js/es' import { history } from 'instantsearch.js/es/lib/routers'; import { createRouterMiddleware } from 'instantsearch.js/es/middlewares'; import { StateMapping, UiState } from 'instantsearch.js/es/types'; type SwagIndexUiState = { swag: boolean }; type SwagUiState = { [indexName: string]: SwagIndexUiState }; const stateMapping: StateMapping<UiState & SwagUiState, SwagUiState> = { stateToRoute(uiState) { return Object.keys(uiState).reduce( (state, indexId) => ({ ...state, [indexId]: { swag: uiState[indexId].swag }, }), {} ); }, routeToState(routeState = {}) { return Object.keys(routeState).reduce( (state, indexId) => ({ ...state, [indexId]: routeState[indexId], }), {} ); }, }; const search = instantsearch(); search.use( createRouterMiddleware<UiState & SwagUiState, SwagUiState>({ router: history(), stateMapping, }) ); search.addWidgets([instantsearch.widgets.hits({ container })]); ``` * actually genericificate InstantSearch * already inferred so no need * Apply suggestions from code review * address feedback * better comment
1 parent 585e38f commit 5f8ba5d

File tree

14 files changed

+199
-149
lines changed

14 files changed

+199
-149
lines changed
 

‎src/index.es.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { InstantSearchOptions } from './types';
1+
import { InstantSearchOptions, UiState } from './types';
22
import InstantSearch from './lib/InstantSearch';
33
import version from './lib/version';
44
import {
@@ -11,8 +11,12 @@ import {
1111
} from './helpers';
1212
import { createInfiniteHitsSessionStorageCache } from './lib/infiniteHitsCache';
1313

14-
const instantsearch = (options: InstantSearchOptions): InstantSearch =>
15-
new InstantSearch(options);
14+
const instantsearch = <
15+
TUiState = Record<string, unknown>,
16+
TRouteState = TUiState
17+
>(
18+
options: InstantSearchOptions<UiState & TUiState, TRouteState>
19+
) => new InstantSearch(options);
1620

1721
instantsearch.version = version;
1822
instantsearch.snippet = snippet;

‎src/lib/InstantSearch.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ function defaultCreateURL() {
4242
/**
4343
* Global options for an InstantSearch instance.
4444
*/
45-
export type InstantSearchOptions = {
45+
export type InstantSearchOptions<
46+
TUiState extends UiState = UiState,
47+
TRouteState = TUiState
48+
> = {
4649
/**
4750
* The name of the main index
4851
*/
@@ -120,7 +123,7 @@ export type InstantSearchOptions = {
120123
* Router configuration used to save the UI State into the URL or any other
121124
* client side persistence. Passing `true` will use the default URL options.
122125
*/
123-
routing?: RouterProps | boolean;
126+
routing?: RouterProps<TUiState, TRouteState> | boolean;
124127

125128
/**
126129
* the instance of search-insights to use for sending insights events inside
@@ -136,7 +139,10 @@ export type InstantSearchOptions = {
136139
* created using the `instantsearch` factory function.
137140
* It emits the 'render' event every time a search is done
138141
*/
139-
class InstantSearch extends EventEmitter {
142+
class InstantSearch<
143+
TUiState extends UiState = UiState,
144+
TRouteState = TUiState
145+
> extends EventEmitter {
140146
public client: InstantSearchOptions['searchClient'];
141147
public indexName: string;
142148
public insightsClient: AlgoliaInsightsClient | null;
@@ -160,7 +166,7 @@ class InstantSearch extends EventEmitter {
160166
}> = [];
161167
public sendEventToInsights: (event: InsightsEvent) => void;
162168

163-
public constructor(options: InstantSearchOptions) {
169+
public constructor(options: InstantSearchOptions<TUiState, TRouteState>) {
164170
super();
165171

166172
const {

‎src/lib/__tests__/RoutingManager-test.ts

+24-20
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import qs from 'qs';
44
import { createSearchClient } from '../../../test/mock/createSearchClient';
55
import { createWidget } from '../../../test/mock/createWidget';
66
import { runAllMicroTasks } from '../../../test/utils/runAllMicroTasks';
7-
import { Router, Widget, UiState, StateMapping, RouteState } from '../../types';
7+
import {
8+
Router,
9+
Widget,
10+
UiState,
11+
StateMapping,
12+
IndexUiState,
13+
} from '../../types';
814
import historyRouter from '../routers/history';
915
import instantsearch from '../main';
1016

@@ -35,32 +41,30 @@ const createFakeStateMapping = (
3541
...args,
3642
});
3743

38-
type Entry = Record<string, unknown>;
39-
40-
type HistoryState = {
44+
type HistoryState<TEntry> = {
4145
index: number;
42-
entries: Entry[];
43-
listeners: Array<(value: Entry) => void>;
46+
entries: TEntry[];
47+
listeners: Array<(value: TEntry) => void>;
4448
};
4549

46-
const createFakeHistory = (
50+
const createFakeHistory = <TEntry = Record<string, unknown>>(
4751
{
4852
index = -1,
4953
entries = [],
5054
listeners = [],
51-
}: HistoryState = {} as HistoryState
55+
}: HistoryState<TEntry> = {} as HistoryState<TEntry>
5256
) => {
53-
const state: HistoryState = {
57+
const state: HistoryState<TEntry> = {
5458
index,
5559
entries,
5660
listeners,
5761
};
5862

5963
return {
60-
subscribe(listener: (entry: Entry) => void) {
64+
subscribe(listener: (entry: TEntry) => void) {
6165
state.listeners.push(listener);
6266
},
63-
push(value: Entry) {
67+
push(value: TEntry) {
6468
state.entries.push(value);
6569
state.index++;
6670
},
@@ -379,7 +383,7 @@ describe('RoutingManager', () => {
379383
test('should keep the UI state up to date on router.update', async () => {
380384
const searchClient = createSearchClient();
381385
const stateMapping = createFakeStateMapping({});
382-
const history = createFakeHistory();
386+
const history = createFakeHistory<UiState>();
383387
const router = createFakeRouter({
384388
onUpdate(fn) {
385389
history.subscribe(state => {
@@ -471,7 +475,7 @@ describe('RoutingManager', () => {
471475
return uiState;
472476
},
473477
});
474-
const history = createFakeHistory();
478+
const history = createFakeHistory<UiState>();
475479
const router = createFakeRouter({
476480
onUpdate(fn) {
477481
history.subscribe(state => {
@@ -549,10 +553,10 @@ describe('RoutingManager', () => {
549553
const searchClient = createSearchClient();
550554
const stateMapping = createFakeStateMapping({});
551555
const router = historyRouter({
552-
windowTitle(routeState: RouteState) {
556+
windowTitle(routeState) {
553557
return `Searching for "${routeState.query}"`;
554558
},
555-
} as any);
559+
});
556560

557561
const search = instantsearch({
558562
indexName: 'instant_search',
@@ -596,7 +600,7 @@ describe('RoutingManager', () => {
596600
url: createFakeUrlWithRefinements({ length: 22 }),
597601
});
598602

599-
const router = historyRouter();
603+
const router = historyRouter<IndexUiState>();
600604
// @ts-expect-error: This method is considered private but we still use it
601605
// in the test after the TypeScript migration.
602606
// In a next refactor, we can consider changing this test implementation.
@@ -605,7 +609,7 @@ describe('RoutingManager', () => {
605609
location: window.location,
606610
});
607611

608-
expect(parsedUrl.refinementList.brand).toBeInstanceOf(Array);
612+
expect(parsedUrl.refinementList!.brand).toBeInstanceOf(Array);
609613
expect(parsedUrl).toMatchInlineSnapshot(`
610614
Object {
611615
"refinementList": Object {
@@ -643,7 +647,7 @@ describe('RoutingManager', () => {
643647
url: createFakeUrlWithRefinements({ length: 100 }),
644648
});
645649

646-
const router = historyRouter();
650+
const router = historyRouter<IndexUiState>();
647651
// @ts-expect-error: This method is considered private but we still use it
648652
// in the test after the TypeScript migration.
649653
// In a next refactor, we can consider changing this test implementation.
@@ -652,13 +656,13 @@ describe('RoutingManager', () => {
652656
location: window.location,
653657
});
654658

655-
expect(parsedUrl.refinementList.brand).toBeInstanceOf(Array);
659+
expect(parsedUrl.refinementList!.brand).toBeInstanceOf(Array);
656660
});
657661
});
658662

659663
describe('createURL', () => {
660664
it('returns an URL for a `routeState` with refinements', () => {
661-
const router = historyRouter();
665+
const router = historyRouter<IndexUiState>();
662666
const actual = router.createURL({
663667
query: 'iPhone',
664668
page: 5,

‎src/lib/main.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as middlewares from '../middlewares/index';
99
import * as routers from './routers/index';
1010
import * as stateMappings from './stateMappings/index';
1111
import { createInfiniteHitsSessionStorageCache } from './infiniteHitsCache/index';
12-
import { InstantSearchOptions } from '../types';
12+
import { InstantSearchOptions, UiState } from '../types';
1313

1414
/**
1515
* InstantSearch is the main component of InstantSearch.js. This object
@@ -28,8 +28,12 @@ import { InstantSearchOptions } from '../types';
2828
* @function instantsearch
2929
* @param {InstantSearchOptions} options The options
3030
*/
31-
const instantsearch = (options: InstantSearchOptions) =>
32-
new InstantSearch(options);
31+
const instantsearch = <
32+
TUiState = Record<string, unknown>,
33+
TRouteState = TUiState
34+
>(
35+
options: InstantSearchOptions<UiState & TUiState, TRouteState>
36+
) => new InstantSearch(options);
3337

3438
instantsearch.routers = routers;
3539
instantsearch.stateMappings = stateMappings;

‎src/lib/routers/__tests__/history.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ describe('life cycle', () => {
1010
it('writes after timeout is done', async () => {
1111
const pushState = jest.spyOn(window.history, 'pushState');
1212

13-
const router = historyRouter({
13+
const router = historyRouter<{ some: string }>({
1414
writeDelay: 0,
1515
});
1616

0 commit comments

Comments
 (0)
Please sign in to comment.