From f436d31184f3f75b33a1fdaa19c665e77948df28 Mon Sep 17 00:00:00 2001 From: Haroen Viaene Date: Mon, 3 Oct 2022 13:28:57 +0200 Subject: [PATCH] feat(useInstantSearch): expose status & error (#3645) **Summary** Introduces `status` and `error` in the response of `useInstantSearch`, as well as a new option on `useInstantSearch` to indicate errors in the search lifecycle should be caught. built on top of https://github.com/algolia/instantsearch.js/pull/5127 **Result** ```jsx function Status() { const { status, error } = useInstantSearch({ catchError: true }); return ( <> Search status: {status} {error && a search error occurred: {error.message}} ); } ``` FX-1769 --- examples/hooks-e-commerce/package.json | 2 +- examples/hooks-react-native/package.json | 2 +- examples/hooks/package.json | 2 +- package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../react-instantsearch-hooks/package.json | 2 +- .../src/hooks/__tests__/useConnector.test.tsx | 2 + .../hooks/__tests__/useInstantSearch.test.tsx | 121 ++++++++++++++++++ .../src/hooks/useConnector.ts | 4 +- .../src/hooks/useInstantSearch.ts | 27 +++- test/utils/InstantSearchHooksTestWrapper.tsx | 14 ++ test/utils/createInstantSearchTestWrapper.tsx | 18 --- test/utils/index.ts | 1 - yarn.lock | 8 +- 15 files changed, 175 insertions(+), 34 deletions(-) delete mode 100644 test/utils/createInstantSearchTestWrapper.tsx diff --git a/examples/hooks-e-commerce/package.json b/examples/hooks-e-commerce/package.json index e495e2c6da..ab75215368 100644 --- a/examples/hooks-e-commerce/package.json +++ b/examples/hooks-e-commerce/package.json @@ -8,7 +8,7 @@ }, "dependencies": { "algoliasearch": "4.11.0", - "instantsearch.js": "4.46.1", + "instantsearch.js": "4.47.0", "react": "18.1.0", "react-compound-slider": "3.4.0", "react-dom": "18.1.0", diff --git a/examples/hooks-react-native/package.json b/examples/hooks-react-native/package.json index c92bdf9127..cdcb67a80d 100644 --- a/examples/hooks-react-native/package.json +++ b/examples/hooks-react-native/package.json @@ -14,7 +14,7 @@ "algoliasearch": "4.12.1", "expo": "~44.0.0", "expo-status-bar": "~1.2.0", - "instantsearch.js": "4.46.1", + "instantsearch.js": "4.47.0", "react": "17.0.1", "react-dom": "17.0.1", "react-instantsearch-hooks": "6.35.0", diff --git a/examples/hooks/package.json b/examples/hooks/package.json index 745d4915a0..3cc9c3493c 100644 --- a/examples/hooks/package.json +++ b/examples/hooks/package.json @@ -8,7 +8,7 @@ }, "dependencies": { "algoliasearch": "4.11.0", - "instantsearch.js": "4.46.1", + "instantsearch.js": "4.47.0", "react": "18.1.0", "react-dom": "18.1.0", "react-instantsearch-hooks-web": "6.35.0" diff --git a/package.json b/package.json index a1d93504a6..1bfa10bf48 100644 --- a/package.json +++ b/package.json @@ -168,7 +168,7 @@ }, { "path": "packages/react-instantsearch-hooks-web/dist/umd/ReactInstantSearchHooksDOM.min.js", - "maxSize": "49.75 kB" + "maxSize": "50 kB" }, { "path": "packages/react-instantsearch-dom/dist/umd/ReactInstantSearchDOM.min.js", diff --git a/packages/react-instantsearch-hooks-server/package.json b/packages/react-instantsearch-hooks-server/package.json index d8ce14af64..0a1aaa6db7 100644 --- a/packages/react-instantsearch-hooks-server/package.json +++ b/packages/react-instantsearch-hooks-server/package.json @@ -46,7 +46,7 @@ }, "dependencies": { "@babel/runtime": "^7.1.2", - "instantsearch.js": "^4.46.1", + "instantsearch.js": "^4.47.0", "react-instantsearch-hooks": "6.35.0" }, "peerDependencies": { diff --git a/packages/react-instantsearch-hooks-web/package.json b/packages/react-instantsearch-hooks-web/package.json index a21f607df3..bbe1b96895 100644 --- a/packages/react-instantsearch-hooks-web/package.json +++ b/packages/react-instantsearch-hooks-web/package.json @@ -47,7 +47,7 @@ }, "dependencies": { "@babel/runtime": "^7.1.2", - "instantsearch.js": "^4.46.1", + "instantsearch.js": "^4.47.0", "react-instantsearch-hooks": "6.35.0" }, "peerDependencies": { diff --git a/packages/react-instantsearch-hooks/package.json b/packages/react-instantsearch-hooks/package.json index 32e19d2769..3d246993e5 100644 --- a/packages/react-instantsearch-hooks/package.json +++ b/packages/react-instantsearch-hooks/package.json @@ -48,7 +48,7 @@ "dependencies": { "@babel/runtime": "^7.1.2", "algoliasearch-helper": "^3.11.1", - "instantsearch.js": "^4.46.1", + "instantsearch.js": "^4.47.0", "use-sync-external-store": "^1.0.0" }, "peerDependencies": { diff --git a/packages/react-instantsearch-hooks/src/hooks/__tests__/useConnector.test.tsx b/packages/react-instantsearch-hooks/src/hooks/__tests__/useConnector.test.tsx index 9b7f44e49c..e8fb4067b3 100644 --- a/packages/react-instantsearch-hooks/src/hooks/__tests__/useConnector.test.tsx +++ b/packages/react-instantsearch-hooks/src/hooks/__tests__/useConnector.test.tsx @@ -384,6 +384,8 @@ describe('useConnector', () => { searchMetadata: { isSearchStalled: false, }, + status: 'idle', + error: undefined, }); }); diff --git a/packages/react-instantsearch-hooks/src/hooks/__tests__/useInstantSearch.test.tsx b/packages/react-instantsearch-hooks/src/hooks/__tests__/useInstantSearch.test.tsx index 0219ce224f..d413810315 100644 --- a/packages/react-instantsearch-hooks/src/hooks/__tests__/useInstantSearch.test.tsx +++ b/packages/react-instantsearch-hooks/src/hooks/__tests__/useInstantSearch.test.tsx @@ -9,6 +9,7 @@ import { createSearchClient } from '../../../../../test/mock'; import { createInstantSearchTestWrapper, InstantSearchHooksTestWrapper, + wait, } from '../../../../../test/utils'; import { useInstantSearch } from '../useInstantSearch'; @@ -286,4 +287,124 @@ describe('useInstantSearch', () => { expect(result.current.refresh).toBe(ref); }); }); + + describe('status', () => { + test('initial status: idle', () => { + const App = () => ( + + + + + ); + + const { getByTestId } = render(); + + expect(getByTestId('status')).toHaveTextContent('idle'); + }); + + test('turns to loading and idle when searching', async () => { + const App = () => ( + + + + + ); + + const { getByTestId, getByPlaceholderText } = render(); + + expect(getByTestId('status')).toHaveTextContent('idle'); + + userEvent.type(getByPlaceholderText('search here'), 'hey search'); + + await waitFor(() => { + expect(getByTestId('status')).toHaveTextContent('loading'); + }); + + await waitFor(() => { + expect(getByTestId('status')).toHaveTextContent('idle'); + }); + }); + + test('turns to loading, stalled and idle when searching slowly', async () => { + const App = () => ( + + + + + ); + + const { getByTestId, getByPlaceholderText } = render(); + + expect(getByTestId('status')).toHaveTextContent('idle'); + + userEvent.type(getByPlaceholderText('search here'), 'h'); + + await waitFor(() => { + expect(getByTestId('status')).toHaveTextContent('loading'); + }); + + await waitFor(() => { + expect(getByTestId('status')).toHaveTextContent('stalled'); + }); + + await waitFor(() => { + expect(getByTestId('status')).toHaveTextContent('idle'); + }); + }); + + test('turns to loading and error when searching', async () => { + const searchClient = createSearchClient({}); + searchClient.search.mockImplementation(() => + Promise.reject(new Error('API_ERROR')) + ); + + const App = () => ( + + + {/* has catchError, as the real error can not be asserted upon */} + + + ); + + const { getByTestId, getByPlaceholderText } = render(); + + expect(getByTestId('status')).toHaveTextContent('idle'); + expect(getByTestId('error')).toBeEmptyDOMElement(); + + userEvent.type(getByPlaceholderText('search here'), 'hey search'); + + await waitFor(() => { + expect(getByTestId('status')).toHaveTextContent('loading'); + expect(getByTestId('error')).toBeEmptyDOMElement(); + }); + + await waitFor(() => { + expect(getByTestId('status')).toHaveTextContent('error'); + expect(getByTestId('error')).toHaveTextContent('API_ERROR'); + }); + }); + + function Status(props) { + const { status, error } = useInstantSearch(props); + + return ( + <> + {status} + {error?.message} + + ); + } + + function createDelayedSearchClient(timeout: number) { + const searchFn = createSearchClient({}).search!; + return createSearchClient({ + search: (requests) => wait(timeout).then(() => searchFn(requests)), + }); + } + }); }); diff --git a/packages/react-instantsearch-hooks/src/hooks/useConnector.ts b/packages/react-instantsearch-hooks/src/hooks/useConnector.ts index 0ac19e3df7..f70612ee5c 100644 --- a/packages/react-instantsearch-hooks/src/hooks/useConnector.ts +++ b/packages/react-instantsearch-hooks/src/hooks/useConnector.ts @@ -115,8 +115,10 @@ export function useConnector< templatesConfig: search.templatesConfig, createURL: parentIndex.createURL, searchMetadata: { - isSearchStalled: search._isSearchStalled, + isSearchStalled: search.status === 'stalled', }, + status: search.status, + error: search.error, }); return renderState; diff --git a/packages/react-instantsearch-hooks/src/hooks/useInstantSearch.ts b/packages/react-instantsearch-hooks/src/hooks/useInstantSearch.ts index a723cd635d..44b9279961 100644 --- a/packages/react-instantsearch-hooks/src/hooks/useInstantSearch.ts +++ b/packages/react-instantsearch-hooks/src/hooks/useInstantSearch.ts @@ -1,6 +1,7 @@ import { useCallback } from 'react'; import { useInstantSearchContext } from '../lib/useInstantSearchContext'; +import { useIsomorphicLayoutEffect } from '../lib/useIsomorphicLayoutEffect'; import { useSearchResults } from '../lib/useSearchResults'; import { useSearchState } from '../lib/useSearchState'; @@ -12,11 +13,20 @@ type InstantSearchApi = SearchStateApi & SearchResultsApi & { use: (...middlewares: Middleware[]) => () => void; refresh: InstantSearch['refresh']; + status: InstantSearch['status']; + error: InstantSearch['error']; }; -export function useInstantSearch< - TUiState extends UiState = UiState ->(): InstantSearchApi { +export type UseInstantSearchProps = { + /** + * catch any error happening in the search lifecycle and handle it with this hook. + */ + catchError?: boolean; +}; + +export function useInstantSearch({ + catchError, +}: UseInstantSearchProps = {}): InstantSearchApi { const search = useInstantSearchContext(); const { uiState, setUiState, indexUiState, setIndexUiState } = useSearchState(); @@ -37,6 +47,15 @@ export function useInstantSearch< search.refresh(); }, [search]); + useIsomorphicLayoutEffect(() => { + if (catchError) { + const onError = () => {}; + search.addListener('error', onError); + return () => search.removeListener('error', onError); + } + return () => {}; + }, [search, catchError]); + return { results, scopedResults, @@ -46,5 +65,7 @@ export function useInstantSearch< setIndexUiState, use, refresh, + status: search.status, + error: search.error, }; } diff --git a/test/utils/InstantSearchHooksTestWrapper.tsx b/test/utils/InstantSearchHooksTestWrapper.tsx index 402730ce18..27ef4be87e 100644 --- a/test/utils/InstantSearchHooksTestWrapper.tsx +++ b/test/utils/InstantSearchHooksTestWrapper.tsx @@ -21,3 +21,17 @@ export function InstantSearchHooksTestWrapper({ ); } + +export function createInstantSearchTestWrapper( + props?: Partial +) { + const client = createSearchClient({}); + + const wrapper = ({ children }) => ( + + {children} + + ); + + return wrapper; +} diff --git a/test/utils/createInstantSearchTestWrapper.tsx b/test/utils/createInstantSearchTestWrapper.tsx deleted file mode 100644 index 74e7dd5615..0000000000 --- a/test/utils/createInstantSearchTestWrapper.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import type { InstantSearchProps } from '../../packages/react-instantsearch-hooks/src'; -import { InstantSearch } from '../../packages/react-instantsearch-hooks/src'; - -import { createSearchClient } from '../mock'; - -export function createInstantSearchTestWrapper( - props?: Partial -) { - const searchClient = createSearchClient({}); - const wrapper = ({ children }) => ( - - {children} - - ); - - return wrapper; -} diff --git a/test/utils/index.ts b/test/utils/index.ts index d26a82eb2e..0780a301c9 100644 --- a/test/utils/index.ts +++ b/test/utils/index.ts @@ -1,5 +1,4 @@ export * from './createInstantSearchSpy'; -export * from './createInstantSearchTestWrapper'; export * from './InstantSearchHooksTestWrapper'; export * from './runAllMicroTasks'; export * from './wait'; diff --git a/yarn.lock b/yarn.lock index 975ea25533..1bd613a79a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16941,10 +16941,10 @@ instantsearch.css@7.4.5: resolved "https://registry.yarnpkg.com/instantsearch.css/-/instantsearch.css-7.4.5.tgz#2a521aa634329bf1680f79adf87c79d67669ec8d" integrity sha512-iIGBYjCokU93DDB8kbeztKtlu4qVEyTg1xvS6iSO1YvqRwkIZgf0tmsl/GytsLdZhuw8j4wEaeYsCzNbeJ/zEQ== -instantsearch.js@4.46.1, instantsearch.js@^4.46.1: - version "4.46.1" - resolved "https://registry.yarnpkg.com/instantsearch.js/-/instantsearch.js-4.46.1.tgz#93f2309e6fb8821b00a00eadcb14c368e713ddca" - integrity sha512-zcuCVtMPwpzmqprtUMh6qF21hJiaipRSA8vbFZlrRfNZMg0gkm5JE2sPPBDoMWMED+1yIIDgoyLzTKRXamJzlQ== +instantsearch.js@4.47.0, instantsearch.js@^4.47.0: + version "4.47.0" + resolved "https://registry.yarnpkg.com/instantsearch.js/-/instantsearch.js-4.47.0.tgz#eb06d4deede956dff9391e577469061e0d7e9c56" + integrity sha512-SoIVDGtqKFtGQxcEr2vKo0SxbRjAksRKUDP+kRvD/J6tqx8qVZdSArY3XwvEOmhqee/0wK4p4ZMAJS+NI8M8cg== dependencies: "@algolia/events" "^4.0.1" "@algolia/ui-components-highlight-vdom" "^1.1.2"