From 094ae4fe07d1e9f14c15d11bfd3548c6acca1071 Mon Sep 17 00:00:00 2001 From: Haroen Viaene Date: Tue, 19 Nov 2024 14:02:34 +0100 Subject: [PATCH 1/4] fix(rendering): ensure resilience against "null" results In some cases `results` in `render` can be null: - no index name passed - stalled render In the vast majority of cases we already handled `results` being `null` or `undefined` just in case, but now i've changed the type to ensure we always catch this problem This issue doesn't have a reproduction, but i believe it now guards for all possible cases of "no results", so: fixes #6441 [CR-7353] --- .../autocomplete/connectAutocomplete.ts | 16 +++++++++------- .../clear-refinements/connectClearRefinements.ts | 2 +- .../connectCurrentRefinements.ts | 4 ++-- .../infinite-hits/connectInfiniteHits.ts | 4 ++-- .../src/lib/utils/getRefinements.ts | 3 ++- .../src/lib/utils/render-args.ts | 2 +- .../src/middlewares/createInsightsMiddleware.ts | 5 ++++- packages/instantsearch.js/src/types/widget.ts | 6 +++--- .../src/widgets/analytics/analytics.ts | 3 +++ 9 files changed, 27 insertions(+), 18 deletions(-) diff --git a/packages/instantsearch.js/src/connectors/autocomplete/connectAutocomplete.ts b/packages/instantsearch.js/src/connectors/autocomplete/connectAutocomplete.ts index 99bfe3af07..095600b86a 100644 --- a/packages/instantsearch.js/src/connectors/autocomplete/connectAutocomplete.ts +++ b/packages/instantsearch.js/src/connectors/autocomplete/connectAutocomplete.ts @@ -183,21 +183,23 @@ search.addWidgets([ const indices = scopedResults.map((scopedResult) => { // We need to escape the hits because highlighting // exposes HTML tags to the end-user. - scopedResult.results.hits = escapeHTML - ? escapeHits(scopedResult.results.hits) - : scopedResult.results.hits; + if (scopedResult.results) { + scopedResult.results.hits = escapeHTML + ? escapeHits(scopedResult.results.hits) + : scopedResult.results.hits; + } const sendEvent = createSendEventForHits({ instantSearchInstance, - getIndex: () => scopedResult.results.index, + getIndex: () => scopedResult.results?.index || '', widgetType: this.$$type, }); return { indexId: scopedResult.indexId, - indexName: scopedResult.results.index, - hits: scopedResult.results.hits, - results: scopedResult.results, + indexName: scopedResult.results?.index || '', + hits: scopedResult.results?.hits || [], + results: scopedResult.results || ({} as unknown as SearchResults), sendEvent, }; }); diff --git a/packages/instantsearch.js/src/connectors/clear-refinements/connectClearRefinements.ts b/packages/instantsearch.js/src/connectors/clear-refinements/connectClearRefinements.ts index 3de2b0d395..b5539cbfc9 100644 --- a/packages/instantsearch.js/src/connectors/clear-refinements/connectClearRefinements.ts +++ b/packages/instantsearch.js/src/connectors/clear-refinements/connectClearRefinements.ts @@ -233,7 +233,7 @@ function getAttributesToClear({ includedAttributes: string[]; excludedAttributes: string[]; transformItems: TransformItems; - results: SearchResults | undefined; + results: SearchResults | undefined | null; }): AttributesToClear { const includesQuery = includedAttributes.indexOf('query') !== -1 || diff --git a/packages/instantsearch.js/src/connectors/current-refinements/connectCurrentRefinements.ts b/packages/instantsearch.js/src/connectors/current-refinements/connectCurrentRefinements.ts index d042d6801a..df541aaed4 100644 --- a/packages/instantsearch.js/src/connectors/current-refinements/connectCurrentRefinements.ts +++ b/packages/instantsearch.js/src/connectors/current-refinements/connectCurrentRefinements.ts @@ -232,7 +232,7 @@ const connectCurrentRefinements: CurrentRefinementsConnector = if (!results) { return transformItems( getRefinementsItems({ - results: {}, + results: null, helper, indexId: helper.state.index, includedAttributes, @@ -282,7 +282,7 @@ function getRefinementsItems({ includedAttributes, excludedAttributes, }: { - results: SearchResults | Record; + results: SearchResults | null; helper: AlgoliaSearchHelper; indexId: string; includedAttributes: CurrentRefinementsConnectorParams['includedAttributes']; diff --git a/packages/instantsearch.js/src/connectors/infinite-hits/connectInfiniteHits.ts b/packages/instantsearch.js/src/connectors/infinite-hits/connectInfiniteHits.ts index 7ed6188ff6..2656199140 100644 --- a/packages/instantsearch.js/src/connectors/infinite-hits/connectInfiniteHits.ts +++ b/packages/instantsearch.js/src/connectors/infinite-hits/connectInfiniteHits.ts @@ -138,7 +138,7 @@ export type InfiniteHitsRenderState< /** * The response from the Algolia API. */ - results?: SearchResults>; + results?: SearchResults> | null; /** * The banner to display above the hits. @@ -435,7 +435,7 @@ export default (function connectInfiniteHits< sendEvent, bindEvent, banner, - results, + results: results || undefined, showPrevious, showMore, isFirstPage, diff --git a/packages/instantsearch.js/src/lib/utils/getRefinements.ts b/packages/instantsearch.js/src/lib/utils/getRefinements.ts index 2f36eea75d..caca0f47a6 100644 --- a/packages/instantsearch.js/src/lib/utils/getRefinements.ts +++ b/packages/instantsearch.js/src/lib/utils/getRefinements.ts @@ -106,10 +106,11 @@ function getRefinement( } export function getRefinements( - results: SearchResults | Record, + _results: SearchResults | Record | null, state: SearchParameters, includesQuery: boolean = false ): Refinement[] { + const results = _results || {}; const refinements: Refinement[] = []; const { facetsRefinements = {}, diff --git a/packages/instantsearch.js/src/lib/utils/render-args.ts b/packages/instantsearch.js/src/lib/utils/render-args.ts index c08cc7ad76..3eaabbcda7 100644 --- a/packages/instantsearch.js/src/lib/utils/render-args.ts +++ b/packages/instantsearch.js/src/lib/utils/render-args.ts @@ -29,7 +29,7 @@ export function createRenderArgs( parent: IndexWidget, widget: IndexWidget | Widget ) { - const results = parent.getResultsForWidget(widget)!; + const results = parent.getResultsForWidget(widget); const helper = parent.getHelper()!; return { diff --git a/packages/instantsearch.js/src/middlewares/createInsightsMiddleware.ts b/packages/instantsearch.js/src/middlewares/createInsightsMiddleware.ts index 3f67199365..2a672e77a0 100644 --- a/packages/instantsearch.js/src/middlewares/createInsightsMiddleware.ts +++ b/packages/instantsearch.js/src/middlewares/createInsightsMiddleware.ts @@ -443,7 +443,10 @@ export function createInsightsMiddleware< instantSearchInstance.mainHelper!.derivedHelpers[0].on( 'result', ({ results }) => { - if (!results.queryID || results.queryID !== lastQueryId) { + if ( + results && + (!results.queryID || results.queryID !== lastQueryId) + ) { lastQueryId = results.queryID; viewedObjectIDs.clear(); } diff --git a/packages/instantsearch.js/src/types/widget.ts b/packages/instantsearch.js/src/types/widget.ts index 031339fc9d..0714853dec 100644 --- a/packages/instantsearch.js/src/types/widget.ts +++ b/packages/instantsearch.js/src/types/widget.ts @@ -13,7 +13,7 @@ import type { export type ScopedResult = { indexId: string; - results: SearchResults; + results: SearchResults | null; helper: Helper; }; @@ -45,7 +45,7 @@ export type InitOptions = SharedRenderOptions & { export type ShouldRenderOptions = { instantSearchInstance: InstantSearch }; export type RenderOptions = SharedRenderOptions & { - results: SearchResults; + results: SearchResults | null; }; export type DisposeOptions = { @@ -339,7 +339,7 @@ export type Widget< export type { IndexWidget } from '../widgets'; export type TransformItemsMetadata = { - results?: SearchResults; + results: SearchResults | undefined | null; }; /** diff --git a/packages/instantsearch.js/src/widgets/analytics/analytics.ts b/packages/instantsearch.js/src/widgets/analytics/analytics.ts index 90bf415723..2299ae3ad7 100644 --- a/packages/instantsearch.js/src/widgets/analytics/analytics.ts +++ b/packages/instantsearch.js/src/widgets/analytics/analytics.ts @@ -252,6 +252,9 @@ For the migration, visit https://www.algolia.com/doc/guides/building-search-ui/u }, render({ results, state }) { + if (!results) { + return; + } if (isInitialSearch === true) { isInitialSearch = false; From caa00b2f19370eecc25075f87a74f4bb92244eb8 Mon Sep 17 00:00:00 2001 From: Haroen Viaene Date: Tue, 19 Nov 2024 14:19:26 +0100 Subject: [PATCH 2/4] type --- packages/react-instantsearch-core/src/lib/useSearchResults.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-instantsearch-core/src/lib/useSearchResults.ts b/packages/react-instantsearch-core/src/lib/useSearchResults.ts index 90862df1c7..d91d69d8dc 100644 --- a/packages/react-instantsearch-core/src/lib/useSearchResults.ts +++ b/packages/react-instantsearch-core/src/lib/useSearchResults.ts @@ -16,7 +16,7 @@ export type SearchResultsApi = { export function useSearchResults(): SearchResultsApi { const search = useInstantSearchContext(); const searchIndex = useIndexContext(); - const [searchResults, setSearchResults] = useState(() => { + const [searchResults, setSearchResults] = useState(() => { const indexSearchResults = getIndexSearchResults(searchIndex); // We do this not to leak `recommendResults` in the API. return { From 6ca1dcd82732d2b27fa377ddbcb56c5722ea2f58 Mon Sep 17 00:00:00 2001 From: Haroen Viaene Date: Tue, 19 Nov 2024 14:40:10 +0100 Subject: [PATCH 3/4] types --- .../js/e-commerce-umd/src/widgets/ClearFiltersEmptyResults.ts | 4 ++-- examples/js/e-commerce-umd/src/widgets/Pagination.ts | 2 +- .../js/e-commerce/src/widgets/ClearFiltersEmptyResults.ts | 4 ++-- examples/js/e-commerce/src/widgets/Pagination.ts | 2 +- .../src/lib/insights/__tests__/insights-client-test.ts | 2 +- packages/instantsearch.js/stories/panel.stories.ts | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/js/e-commerce-umd/src/widgets/ClearFiltersEmptyResults.ts b/examples/js/e-commerce-umd/src/widgets/ClearFiltersEmptyResults.ts index ea8ac18541..38cb493d1f 100644 --- a/examples/js/e-commerce-umd/src/widgets/ClearFiltersEmptyResults.ts +++ b/examples/js/e-commerce-umd/src/widgets/ClearFiltersEmptyResults.ts @@ -1,8 +1,8 @@ const { panel, clearRefinements } = window.instantsearch.widgets; const clearFilters = panel({ - hidden(options) { - return options.results.nbHits > 0; + hidden({ results }) { + return Boolean(results && results.nbHits > 0); }, })(clearRefinements); diff --git a/examples/js/e-commerce-umd/src/widgets/Pagination.ts b/examples/js/e-commerce-umd/src/widgets/Pagination.ts index 07f58e3289..6e77cdabbc 100644 --- a/examples/js/e-commerce-umd/src/widgets/Pagination.ts +++ b/examples/js/e-commerce-umd/src/widgets/Pagination.ts @@ -2,7 +2,7 @@ const { pagination: paginationWidget, panel } = window.instantsearch.widgets; const paginationWithMultiplePages = panel({ hidden({ results }) { - return results.nbPages <= 1; + return Boolean(results && results.nbPages <= 1); }, })(paginationWidget); diff --git a/examples/js/e-commerce/src/widgets/ClearFiltersEmptyResults.ts b/examples/js/e-commerce/src/widgets/ClearFiltersEmptyResults.ts index db0a762f2d..b1d10f543a 100644 --- a/examples/js/e-commerce/src/widgets/ClearFiltersEmptyResults.ts +++ b/examples/js/e-commerce/src/widgets/ClearFiltersEmptyResults.ts @@ -1,8 +1,8 @@ import { panel, clearRefinements } from 'instantsearch.js/es/widgets'; const clearFilters = panel({ - hidden(options) { - return options.results.nbHits > 0; + hidden({ results }) { + return Boolean(results && results.nbHits > 0); }, })(clearRefinements); diff --git a/examples/js/e-commerce/src/widgets/Pagination.ts b/examples/js/e-commerce/src/widgets/Pagination.ts index bfd5206c13..d6a97e39aa 100644 --- a/examples/js/e-commerce/src/widgets/Pagination.ts +++ b/examples/js/e-commerce/src/widgets/Pagination.ts @@ -5,7 +5,7 @@ import { const paginationWithMultiplePages = panel({ hidden({ results }) { - return results.nbPages <= 1; + return Boolean(results && results.nbPages <= 1); }, })(paginationWidget); diff --git a/packages/instantsearch.js/src/lib/insights/__tests__/insights-client-test.ts b/packages/instantsearch.js/src/lib/insights/__tests__/insights-client-test.ts index dc6a94a6c7..bddce6db68 100644 --- a/packages/instantsearch.js/src/lib/insights/__tests__/insights-client-test.ts +++ b/packages/instantsearch.js/src/lib/insights/__tests__/insights-client-test.ts @@ -12,7 +12,7 @@ const connectHits = $$type: 'ais.hits', init() {}, render({ results, instantSearchInstance }) { - const hits = results.hits; + const hits = results?.hits; renderFn({ hits, results, instantSearchInstance, widgetParams }, false); }, dispose() { diff --git a/packages/instantsearch.js/stories/panel.stories.ts b/packages/instantsearch.js/stories/panel.stories.ts index 84d3d8e4c3..068b86206c 100644 --- a/packages/instantsearch.js/stories/panel.stories.ts +++ b/packages/instantsearch.js/stories/panel.stories.ts @@ -90,7 +90,7 @@ storiesOf('Basics/Panel', module) header: () => 'Price', footer: () => 'The panel is hidden when there are no results.', }, - hidden: ({ results }) => results.nbHits === 0, + hidden: ({ results }) => results?.nbHits === 0, })(instantsearch.widgets.rangeInput); search.addWidgets([ From 2cd1ddc00c621f4d2c291550ecb348a156d94752 Mon Sep 17 00:00:00 2001 From: Haroen Viaene Date: Tue, 19 Nov 2024 15:00:17 +0100 Subject: [PATCH 4/4] margin --- bundlesize.config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundlesize.config.json b/bundlesize.config.json index 744e2f5b46..79877f52d0 100644 --- a/bundlesize.config.json +++ b/bundlesize.config.json @@ -10,11 +10,11 @@ }, { "path": "./packages/instantsearch.js/dist/instantsearch.production.min.js", - "maxSize": "83.50 kB" + "maxSize": "84 kB" }, { "path": "./packages/instantsearch.js/dist/instantsearch.development.js", - "maxSize": "181.50 kB" + "maxSize": "182 kB" }, { "path": "packages/react-instantsearch-core/dist/umd/ReactInstantSearchCore.min.js",