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

Commit

Permalink
feat(hooks): migrate to useSyncExternalStore() (#3489)
Browse files Browse the repository at this point in the history
  • Loading branch information
francoischalifour authored Jun 1, 2022
1 parent 2cf0ebe commit 81bbdf2
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 700 deletions.
2 changes: 2 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ module.exports = (api) => {
'instantsearch.js',
// React-DOM also fails if the paths are incomplete
'react-dom',
// `use-sync-external-store` also fails if the paths are incomplete
'use-sync-external-store',
],
},
],
Expand Down
8 changes: 7 additions & 1 deletion packages/react-instantsearch-hooks-web/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ const plugins = [
preferBuiltins: false,
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'],
}),
commonjs(),
commonjs({
namedExports: {
'../../node_modules/use-sync-external-store/shim/index.js': [
'useSyncExternalStore',
],
},
}),
globals(),
replace({
'process.env.NODE_ENV': JSON.stringify('production'),
Expand Down
3 changes: 2 additions & 1 deletion packages/react-instantsearch-hooks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
"dependencies": {
"@babel/runtime": "^7.1.2",
"algoliasearch-helper": "^3.7.4",
"instantsearch.js": "^4.40.1"
"instantsearch.js": "^4.40.1",
"use-sync-external-store": "^1.0.0"
},
"peerDependencies": {
"algoliasearch": ">= 3.1 < 5",
Expand Down
8 changes: 7 additions & 1 deletion packages/react-instantsearch-hooks/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ const plugins = [
preferBuiltins: false,
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'],
}),
commonjs(),
commonjs({
namedExports: {
'../../node_modules/use-sync-external-store/shim/index.js': [
'useSyncExternalStore',
],
},
}),
globals(),
replace({
'process.env.NODE_ENV': JSON.stringify('production'),
Expand Down
28 changes: 9 additions & 19 deletions packages/react-instantsearch-hooks/src/hooks/useConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,25 +83,11 @@ export function useConnector<
}
);

const instance = {
return {
...createWidget(stableProps),
...stableAdditionalWidgetProperties,
};

// On the server, we add the widget early in the memo to retrieve its search
// parameters in the render pass.
if (serverContext) {
parentIndex.addWidgets([instance]);
}

return instance;
}, [
connector,
parentIndex,
serverContext,
stableProps,
stableAdditionalWidgetProperties,
]);
}, [connector, stableProps, stableAdditionalWidgetProperties]);

const [state, setState] = useState<TDescription['renderState']>(() => {
if (widget.getWidgetRenderState) {
Expand Down Expand Up @@ -159,15 +145,19 @@ export function useConnector<
return {};
});

// Using a layout effect adds the widget at the same time as rendering, which
// triggers a single network request, instead of two with a regular effect.
useIsomorphicLayoutEffect(() => {
parentIndex.addWidgets([widget]);

return () => {
parentIndex.removeWidgets([widget]);
};
}, [widget, parentIndex]);
}, [parentIndex, widget]);

// On the server, we add the widget early to retrieve its search parameters
// in the render pass.
if (serverContext && !parentIndex.getWidgets().includes(widget)) {
parentIndex.addWidgets([widget]);
}

return state;
}
104 changes: 45 additions & 59 deletions packages/react-instantsearch-hooks/src/lib/useInstantSearch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import InstantSearch from 'instantsearch.js/es/lib/InstantSearch';
import { useEffect, useMemo, version as ReactVersion } from 'react';
import { useCallback, useMemo, version as ReactVersion } from 'react';
import { useSyncExternalStore } from 'use-sync-external-store/shim';

import { useInstantSearchServerContext } from '../lib/useInstantSearchServerContext';
import { useInstantSearchSSRContext } from '../lib/useInstantSearchSSRContext';
Expand All @@ -8,8 +9,6 @@ import version from '../version';
import { useForceUpdate } from './useForceUpdate';
import { useStableValue } from './useStableValue';

import type { InstantSearchServerContextApi } from '../components/InstantSearchServerContext';
import type { InstantSearchServerState } from '../components/InstantSearchSSRProvider';
import type {
InstantSearchOptions,
SearchClient,
Expand All @@ -30,84 +29,71 @@ export type UseInstantSearchProps<
export function useInstantSearch<TUiState extends UiState, TRouteState>(
props: UseInstantSearchProps<TUiState, TRouteState>
) {
const forceUpdate = useForceUpdate();
const serverContext = useInstantSearchServerContext();
const serverState = useInstantSearchSSRContext();
const stableProps = useStableValue(props);
const search = useMemo(
() =>
serverAdapter(
new InstantSearch(stableProps),
stableProps,
serverContext,
serverState
),
[stableProps, serverContext, serverState]
);
const forceUpdate = useForceUpdate();
const search = useMemo(() => {
const instance = new InstantSearch(stableProps);
const initialResults = serverState?.initialResults;

if (serverContext || initialResults) {
// InstantSearch.js has a private Initial Results API that lets us inject
// results on the search instance.
// On the server, we default the initial results to an empty object so that
// InstantSearch.js doesn't schedule a search that isn't used, leading to
// an additional network request. (This is equivalent to monkey-patching
// `scheduleSearch` to a noop.)
instance._initialResults = initialResults || {};
}

useEffect(() => {
addAlgoliaAgents(stableProps.searchClient, defaultUserAgents);
}, [stableProps.searchClient]);
addAlgoliaAgents(props.searchClient, [
...defaultUserAgents,
serverContext && `react-instantsearch-server (${version})`,
]);

useEffect(() => {
// On SSR, the instance is already started so we don't start it again here.
if (!search.started) {
return instance;
}, [
props.searchClient,
serverContext,
serverState?.initialResults,
stableProps,
]);

const store = useSyncExternalStore<InstantSearch<TUiState, TRouteState>>(
useCallback(() => {
search.start();
forceUpdate();
}

return () => {
search.dispose();
};
}, [search, serverState, forceUpdate]);

return search;
}
return () => {
search.dispose();
};
}, [forceUpdate, search]),
() => search,
() => search
);

function serverAdapter<TUiState extends UiState, TRouteState>(
search: InstantSearch,
props: UseInstantSearchProps<TUiState, TRouteState>,
serverContext: InstantSearchServerContextApi | null,
serverState: Partial<InstantSearchServerState> | null
): InstantSearch<TUiState, TRouteState> {
const initialResults = serverState?.initialResults;

if (serverContext || initialResults) {
// InstantSearch.js has a private Initial Results API that lets us inject
// results on the search instance.
// On the server, we default the initial results to an empty object so that
// InstantSearch.js doesn't schedule a search that isn't used, leading to
// an additional network request. (This is equivalent to monkey-patching
// `scheduleSearch` to a noop.)
search._initialResults = initialResults || {};
if (serverContext && !search.started) {
// On the server, we start the search early to compute the search parameters.
// On SSR, we start the search early to directly catch up with the lifecycle
// and render.
search.start();
}

if (serverContext) {
// On the browser, we add user agents in an effect. Since effects are not
// run on the server, we need to add user agents directly here.
addAlgoliaAgents(props.searchClient, [
...defaultUserAgents,
`react-instantsearch-server (${version})`,
]);

// We notify `getServerState()` of the InstantSearch internals to retrieve
// the server state and pass it to the render on SSR.
serverContext.notifyServer({ search });
}

return search;
return store;
}

function addAlgoliaAgents(searchClient: SearchClient, userAgents: string[]) {
function addAlgoliaAgents(
searchClient: SearchClient,
userAgents: Array<string | null>
) {
if (typeof searchClient.addAlgoliaAgent !== 'function') {
return;
}

userAgents.forEach((userAgent) => {
searchClient.addAlgoliaAgent!(userAgent);
userAgents.filter(Boolean).forEach((userAgent) => {
searchClient.addAlgoliaAgent!(userAgent!);
});
}
Loading

0 comments on commit 81bbdf2

Please sign in to comment.