Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(core): move options diff to core #5908

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
102 changes: 99 additions & 3 deletions packages/instantsearch.js/src/lib/InstantSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ export type InstantSearchStatus = 'idle' | 'loading' | 'stalled' | 'error';
export const INSTANTSEARCH_FUTURE_DEFAULTS: Required<
InstantSearchOptions['future']
> = { preserveSharedStateOnUnmount: false };
export const INSTANTSEARCH_STALLED_SEARCH_DEFAULTS = 200;

/**
* The actual implementation of the InstantSearch. This is
Expand Down Expand Up @@ -250,7 +251,7 @@ Use \`InstantSearch.status === "stalled"\` instead.`
routing = null,
insights = undefined,
searchFunction,
stalledSearchDelay = 200,
stalledSearchDelay = INSTANTSEARCH_STALLED_SEARCH_DEFAULTS,
searchClient = null,
insightsClient = null,
onStateChange = null,
Expand Down Expand Up @@ -364,8 +365,9 @@ See documentation: ${createDocumentationLink({
// This is the default Insights middleware,
// added when `insights` is set to true by the user.
// Any user-provided middleware will be added later and override this one.
if (insights) {
const insightsOptions = typeof insights === 'boolean' ? {} : insights;
if (this._insights) {
const insightsOptions =
typeof this._insights === 'boolean' ? {} : this._insights;
insightsOptions.$$internal = true;
this.use(createInsightsMiddleware(insightsOptions));
}
Expand Down Expand Up @@ -840,6 +842,100 @@ See documentation: ${createDocumentationLink({

this.mainHelper.clearCache().search();
}

/**
* Update the parameters passed to the InstantSearch constructor.
*/
public update(
options: Partial<InstantSearchOptions<TUiState, TRouteState>>
): void {
if (this.indexName !== options.indexName) {
this.helper!.setIndex(options.indexName || '').search();
}

if (this.client !== options.searchClient) {
warning(
false,
// TODO: flavor-specific warning (search._flavor = 'vue') URL
'The `searchClient` parameter changed, which may cause more search requests than necessary. If this is an unwanted behavior, please provide a stable reference.'
);

if (!options.searchClient) {
throw new Error(withUsage('The `searchClient` option is required.'));
}

if (typeof options.searchClient.search !== 'function') {
throw new Error(
`The \`searchClient\` must implement a \`search\` method.

See: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-instantsearch/js/`
);
}

// TODO: proper API for this too
// InstantSearch._algoliaAgents (or constructor)
// addAlgoliaAgents(props.searchClient, [
// ...defaultUserAgents,
// serverContext && serverUserAgent,
// ]);

this.mainHelper!.setClient(options.searchClient).search();
}

if (this.onStateChange !== options.onStateChange) {
this.onStateChange = options.onStateChange;
}

if (this._searchFunction !== options.searchFunction) {
this._searchFunction =
options.searchFunction ??
((helper) => {
helper.search();
});
}

if (this._stalledSearchDelay !== options.stalledSearchDelay) {
this._stalledSearchDelay =
options.stalledSearchDelay ?? INSTANTSEARCH_STALLED_SEARCH_DEFAULTS;
}

// TODO: move dequal to a shared package?
// if (!dequal(this.future, props.future)) {
// this.future = {
// ...INSTANTSEARCH_FUTURE_DEFAULTS,
// ...props.future,
// };
// }

// TODO: implement update in middleware
// TODO: can this be simplified?
// if (!dequal(this._insights, options.insights)) {
// this._insights = options.insights;
// const existing = this.middleware.find(
// (m) => m.instance.$$type === 'ais.insights' && m.instance.$$internal
// );
// if (options.insights && existing) {
// // TODO: update existing middleware somehow (or maybe remount it)
// } else if (options.insights && !existing) {
// const insightsOptions =
// typeof options.insights === 'boolean' ? {} : options.insights;
// insightsOptions.$$internal = true;
// this.use(createInsightsMiddleware(insightsOptions));
// } else if (!options.insights && existing) {
// this.unuse(existing.creator);
// }
// }
Comment on lines +910 to +927
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love for middlewares to remove some of the load from this method. Maybe we could iterate over middlewares, call their update() method, and get a returned value (boolean? object?) which could tell InstantSearch.update() to unuse them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in this case probably existing.update(options.insights) and handling that somehow in the middleware indeed was best, wasn't sure whether the api was worth the coals


// TODO: validate other options (onStateChange, routing, give a warning for all non-updated values?)

// Updating the `routing` prop is not supported because InstantSearch.js
// doesn't let us change it. This might not be a problem though, because `routing`
// shouldn't need to be dynamic.
// If we find scenarios where `routing` needs to change, we can always expose
// it privately on the InstantSearch instance. Another way would be to
// manually inject the routing middleware in this library, and not rely
// on the provided `routing` prop.
Comment on lines +931 to +937
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still can't see why routing would need to change during the lifecycle of an InstantSearch app 😅.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I'd rather ignore or show an error than handling it

}
}

export default InstantSearch;
66 changes: 2 additions & 64 deletions packages/react-instantsearch-core/src/lib/useInstantSearchApi.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import InstantSearch, {
INSTANTSEARCH_FUTURE_DEFAULTS,
} from 'instantsearch.js/es/lib/InstantSearch';
import InstantSearch from 'instantsearch.js/es/lib/InstantSearch';
import { useCallback, useRef, version as ReactVersion } from 'react';
import { useSyncExternalStore } from 'use-sync-external-store/shim';

import version from '../version';

import { dequal } from './dequal';
import { useForceUpdate } from './useForceUpdate';
import { useInstantSearchServerContext } from './useInstantSearchServerContext';
import { useInstantSearchSSRContext } from './useInstantSearchSSRContext';
Expand Down Expand Up @@ -62,7 +59,6 @@ export function useInstantSearchApi<TUiState extends UiState, TRouteState>(
const serverState = useInstantSearchSSRContext<TUiState, TRouteState>();
const waitingForResultsRef = useRSCContext();
const initialResults = serverState?.initialResults;
const prevPropsRef = useRef(props);

const shouldRenderAtOnce =
serverContext || initialResults || waitingForResultsRef;
Expand Down Expand Up @@ -134,65 +130,7 @@ export function useInstantSearchApi<TUiState extends UiState, TRouteState>(
searchRef.current = search;
}

{
const search = searchRef.current;
const prevProps = prevPropsRef.current;

if (prevProps.indexName !== props.indexName) {
search.helper!.setIndex(props.indexName || '').search();
prevPropsRef.current = props;
}

if (prevProps.searchClient !== props.searchClient) {
warn(
false,
'The `searchClient` prop of `<InstantSearch>` changed between renders, which may cause more search requests than necessary. If this is an unwanted behavior, please provide a stable reference: https://www.algolia.com/doc/api-reference/widgets/instantsearch/react/#widget-param-searchclient'
);

addAlgoliaAgents(props.searchClient, [
...defaultUserAgents,
serverContext && serverUserAgent,
]);
search.mainHelper!.setClient(props.searchClient).search();
prevPropsRef.current = props;
}

if (prevProps.onStateChange !== props.onStateChange) {
search.onStateChange = props.onStateChange;
prevPropsRef.current = props;
}

if (prevProps.searchFunction !== props.searchFunction) {
// Updating the `searchFunction` to `undefined` is not supported by
// InstantSearch.js, so it will throw an error.
// This is a fair behavior until we add an update API in InstantSearch.js.
search._searchFunction = props.searchFunction;
prevPropsRef.current = props;
}

if (prevProps.stalledSearchDelay !== props.stalledSearchDelay) {
// The default `stalledSearchDelay` in InstantSearch.js is 200ms.
// We need to reset it when it's undefined to get back to the original value.
search._stalledSearchDelay = props.stalledSearchDelay ?? 200;
prevPropsRef.current = props;
}

if (!dequal(prevProps.future, props.future)) {
search.future = {
...INSTANTSEARCH_FUTURE_DEFAULTS,
...props.future,
};
prevPropsRef.current = props;
}

// Updating the `routing` prop is not supported because InstantSearch.js
// doesn't let us change it. This might not be a problem though, because `routing`
// shouldn't need to be dynamic.
// If we find scenarios where `routing` needs to change, we can always expose
// it privately on the InstantSearch instance. Another way would be to
// manually inject the routing middleware in this library, and not rely
// on the provided `routing` prop.
}
searchRef.current.update(props);

const cleanupTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const store = useSyncExternalStore<InstantSearch<TUiState, TRouteState>>(
Expand Down
25 changes: 10 additions & 15 deletions packages/vue-instantsearch/src/util/createInstantSearchComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,18 @@ export const createInstantSearchComponent = (component) =>
},
watch: {
searchClient(searchClient) {
warn(
false,
'The `search-client` prop of `<ais-instant-search>` changed between renders, which may cause more search requests than necessary. If this is an unwanted behavior, please provide a stable reference: https://www.algolia.com/doc/api-reference/widgets/instantsearch/vue/#widget-param-search-client'
);

this.instantSearchInstance.helper.setClient(searchClient).search();
this.instantSearchInstance.update({ searchClient });
},
indexName(indexName) {
this.instantSearchInstance.helper.setIndex(indexName || '').search();
this.instantSearchInstance.update({ indexName });
},
stalledSearchDelay(stalledSearchDelay) {
// private InstantSearch.js API:
this.instantSearchInstance._stalledSearchDelay = stalledSearchDelay;
this.instantSearchInstance.update({ stalledSearchDelay });
},
routing() {
// TODO: in React this is ignored, in Vue an error
// How do we get a consistent behaviour?
// Nobody ever opened these issues, implying error is right behaviour?
throw new Error(
'routing configuration can not be changed dynamically at this point.' +
'\n\n' +
Expand All @@ -47,9 +44,10 @@ export const createInstantSearchComponent = (component) =>
);
},
searchFunction(searchFunction) {
// private InstantSearch.js API:
this.instantSearchInstance._searchFunction = searchFunction;
this.instantSearchInstance.update({ searchFunction });
},
// TODO: insights
// TODO: should this be an InstantSearch API? maybe not
middlewares: {
immediate: true,
handler(next, prev) {
Expand All @@ -67,10 +65,7 @@ export const createInstantSearchComponent = (component) =>
},
},
future(future) {
this.instantSearchInstance.future = Object.assign(
INSTANTSEARCH_FUTURE_DEFAULTS,
future
);
this.instantSearchInstance.update({ future });
},
},
created() {
Expand Down