Skip to content
This repository has been archived by the owner on Dec 30, 2022. It is now read-only.

Commit

Permalink
feat(useInstantSearch): expose status & error (#3645)
Browse files Browse the repository at this point in the history
<!--
  Thanks for submitting a pull request!
Please provide enough information so that others can review your pull
request.
-->

**Summary**

<!--
  Explain the **motivation** for making this change.
  What existing problem does the pull request solve?
  Are there any linked issues?
-->

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 algolia/instantsearch#5127


**Result**

<!--
  Demonstrate the code is solid.
  Example: The exact commands you ran and their output,
  screenshots / videos if the pull request changes UI.
-->


```jsx
function Status() {
  const { status, error } = useInstantSearch({ catchError: true });

  return (
    <>
      <span>Search status: {status}</span>
      {error && <span>a search error occurred: {error.message}</span>}
    </>
  );
}
```


FX-1769
  • Loading branch information
Haroenv authored Oct 3, 2022
1 parent 980ad70 commit f436d31
Show file tree
Hide file tree
Showing 15 changed files with 175 additions and 34 deletions.
2 changes: 1 addition & 1 deletion examples/hooks-e-commerce/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion examples/hooks-react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion examples/hooks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/react-instantsearch-hooks-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-instantsearch-hooks-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-instantsearch-hooks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,8 @@ describe('useConnector', () => {
searchMetadata: {
isSearchStalled: false,
},
status: 'idle',
error: undefined,
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createSearchClient } from '../../../../../test/mock';
import {
createInstantSearchTestWrapper,
InstantSearchHooksTestWrapper,
wait,
} from '../../../../../test/utils';
import { useInstantSearch } from '../useInstantSearch';

Expand Down Expand Up @@ -286,4 +287,124 @@ describe('useInstantSearch', () => {
expect(result.current.refresh).toBe(ref);
});
});

describe('status', () => {
test('initial status: idle', () => {
const App = () => (
<InstantSearchHooksTestWrapper>
<SearchBox />
<Status />
</InstantSearchHooksTestWrapper>
);

const { getByTestId } = render(<App />);

expect(getByTestId('status')).toHaveTextContent('idle');
});

test('turns to loading and idle when searching', async () => {
const App = () => (
<InstantSearchHooksTestWrapper
searchClient={createDelayedSearchClient(20)}
>
<SearchBox placeholder="search here" />
<Status />
</InstantSearchHooksTestWrapper>
);

const { getByTestId, getByPlaceholderText } = render(<App />);

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 = () => (
<InstantSearchHooksTestWrapper
searchClient={createDelayedSearchClient(300)}
stalledSearchDelay={200}
>
<SearchBox placeholder="search here" />
<Status />
</InstantSearchHooksTestWrapper>
);

const { getByTestId, getByPlaceholderText } = render(<App />);

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 = () => (
<InstantSearchHooksTestWrapper searchClient={searchClient}>
<SearchBox placeholder="search here" />
{/* has catchError, as the real error can not be asserted upon */}
<Status catchError />
</InstantSearchHooksTestWrapper>
);

const { getByTestId, getByPlaceholderText } = render(<App />);

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 (
<>
<span data-testid="status">{status}</span>
<span data-testid="error">{error?.message}</span>
</>
);
}

function createDelayedSearchClient(timeout: number) {
const searchFn = createSearchClient({}).search!;
return createSearchClient({
search: (requests) => wait(timeout).then(() => searchFn(requests)),
});
}
});
});
4 changes: 3 additions & 1 deletion packages/react-instantsearch-hooks/src/hooks/useConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
27 changes: 24 additions & 3 deletions packages/react-instantsearch-hooks/src/hooks/useInstantSearch.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -12,11 +13,20 @@ type InstantSearchApi<TUiState extends UiState> = SearchStateApi<TUiState> &
SearchResultsApi & {
use: (...middlewares: Middleware[]) => () => void;
refresh: InstantSearch['refresh'];
status: InstantSearch['status'];
error: InstantSearch['error'];
};

export function useInstantSearch<
TUiState extends UiState = UiState
>(): InstantSearchApi<TUiState> {
export type UseInstantSearchProps = {
/**
* catch any error happening in the search lifecycle and handle it with this hook.
*/
catchError?: boolean;
};

export function useInstantSearch<TUiState extends UiState = UiState>({
catchError,
}: UseInstantSearchProps = {}): InstantSearchApi<TUiState> {
const search = useInstantSearchContext<TUiState>();
const { uiState, setUiState, indexUiState, setIndexUiState } =
useSearchState<TUiState>();
Expand All @@ -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,
Expand All @@ -46,5 +65,7 @@ export function useInstantSearch<
setIndexUiState,
use,
refresh,
status: search.status,
error: search.error,
};
}
14 changes: 14 additions & 0 deletions test/utils/InstantSearchHooksTestWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,17 @@ export function InstantSearchHooksTestWrapper({
</InstantSearch>
);
}

export function createInstantSearchTestWrapper(
props?: Partial<InstantSearchProps>
) {
const client = createSearchClient({});

const wrapper = ({ children }) => (
<InstantSearch searchClient={client} indexName="indexName" {...props}>
{children}
</InstantSearch>
);

return wrapper;
}
18 changes: 0 additions & 18 deletions test/utils/createInstantSearchTestWrapper.tsx

This file was deleted.

1 change: 0 additions & 1 deletion test/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export * from './createInstantSearchSpy';
export * from './createInstantSearchTestWrapper';
export * from './InstantSearchHooksTestWrapper';
export * from './runAllMicroTasks';
export * from './wait';
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit f436d31

Please sign in to comment.