react-instantsearch-core
Version:
⚡ Lightning-fast search for React, by Algolia
209 lines (204 loc) • 12.4 kB
JavaScript
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
import InstantSearch, { INSTANTSEARCH_FUTURE_DEFAULTS } from "instantsearch.js/es/lib/InstantSearch.js";
import { useCallback, useRef, version as ReactVersion } from 'react';
import { useSyncExternalStore } from "use-sync-external-store/shim/index.js";
import version from "../version.js";
import { dequal } from "./dequal.js";
import { useForceUpdate } from "./useForceUpdate.js";
import { useInstantSearchServerContext } from "./useInstantSearchServerContext.js";
import { useInstantSearchSSRContext } from "./useInstantSearchSSRContext.js";
import { useRSCContext } from "./useRSCContext.js";
import { warn } from "./warn.js";
var defaultUserAgents = ["react (".concat(ReactVersion, ")"), "react-instantsearch (".concat(version, ")"), "react-instantsearch-core (".concat(version, ")")];
var serverUserAgent = "react-instantsearch-server (".concat(version, ")");
var nextUserAgent = function nextUserAgent(nextVersion) {
return nextVersion ? "next.js (".concat(nextVersion, ")") : null;
};
export function useInstantSearchApi(props) {
var forceUpdate = useForceUpdate();
var serverContext = useInstantSearchServerContext();
var serverState = useInstantSearchSSRContext();
var waitingForResultsRef = useRSCContext();
var initialResults = serverState === null || serverState === void 0 ? void 0 : serverState.initialResults;
var prevPropsRef = useRef(props);
var shouldRenderAtOnce = serverContext || initialResults || waitingForResultsRef;
var searchRef = useRef(null);
// As we need to render on mount with SSR, using the local ref above in `StrictMode` will
// create and start two instances of InstantSearch. To avoid this, we instead discard it and use
// an upward ref from `InstantSearchSSRContext` as it has already been mounted a second time at this point.
if (serverState) {
searchRef = serverState.ssrSearchRef;
}
if (searchRef.current === null) {
// We don't use the `instantsearch()` function because it comes with other
// top-level APIs that we don't need.
// See https://github.com/algolia/instantsearch/blob/5b529f43d8acc680f85837eaaa41f7fd03a3f833/src/index.es.ts#L63-L86
var search = new InstantSearch(props);
search._schedule = function _schedule(cb) {
search._schedule.queue.push(cb);
clearTimeout(search._schedule.timer);
search._schedule.timer = setTimeout(function () {
search._schedule.queue.forEach(function (callback) {
callback();
});
search._schedule.queue = [];
}, 0);
};
search._schedule.queue = [];
if (shouldRenderAtOnce) {
// 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 || {};
// We don't rely on the `defer` to reset the schedule search, but will call
// `search._resetScheduleSearch()` manually in the effect after children
// mount in `InstantSearch`.
search._manuallyResetScheduleSearch = true;
}
addAlgoliaAgents(props.searchClient, [].concat(defaultUserAgents, [serverContext && serverUserAgent, nextUserAgent(getNextVersion())]));
// 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.
if (shouldRenderAtOnce) {
search.start();
}
if (serverContext) {
// We notify `getServerState()` of the InstantSearch internals to retrieve
// the server state and pass it to the render on SSR.
serverContext.notifyServer({
search: search
});
}
warnNextRouter(props.routing);
warnNextAppDir(Boolean(waitingForResultsRef));
searchRef.current = search;
}
{
var _search = searchRef.current;
var prevProps = prevPropsRef.current;
if (prevProps.indexName !== props.indexName) {
_search.helper.setIndex(props.indexName || '').search();
prevPropsRef.current = props;
}
if (prevProps.searchClient !== props.searchClient) {
process.env.NODE_ENV === 'development' ? 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') : void 0;
addAlgoliaAgents(props.searchClient, [].concat(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) {
var _props$stalledSearchD;
// 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$stalledSearchD = props.stalledSearchDelay) !== null && _props$stalledSearchD !== void 0 ? _props$stalledSearchD : 200;
prevPropsRef.current = props;
}
if (!dequal(prevProps.future, props.future)) {
_search.future = _objectSpread(_objectSpread({}, 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.
}
var cleanupTimerRef = useRef(null);
var store = useSyncExternalStore(useCallback(function () {
var search = searchRef.current;
// Scenario 1: the component mounts.
if (cleanupTimerRef.current === null) {
// On SSR, the instance is already started so we don't start it again.
if (!search.started) {
search.start();
forceUpdate();
}
}
// Scenario 2: the component updates.
else {
// We cancel the previous cleanup function because we don't want to
// dispose the search during an update.
clearTimeout(cleanupTimerRef.current);
search._preventWidgetCleanup = false;
}
return function () {
function cleanup() {
search.dispose();
}
clearTimeout(search._schedule.timer);
// We clean up only when the component that uses this subscription unmounts,
// but not when it updates, because it would dispose the instance, which
// would remove all the widgets and break routing.
// Executing the cleanup function in a `setTimeout()` lets us cancel it
// in the next effect.
// (There might be better ways to do this.)
cleanupTimerRef.current = setTimeout(cleanup);
// We need to prevent the `useWidget` cleanup function so that widgets
// are not removed before the instance is disposed, triggering
// an unwanted search request.
search._preventWidgetCleanup = true;
};
}, [forceUpdate]), function () {
return searchRef.current;
}, function () {
return searchRef.current;
});
return store;
}
function addAlgoliaAgents(searchClient, userAgents) {
if (typeof searchClient.addAlgoliaAgent !== 'function') {
return;
}
userAgents.filter(Boolean).forEach(function (userAgent) {
searchClient.addAlgoliaAgent(userAgent);
});
}
function warnNextRouter(routing) {
if (process.env.NODE_ENV === 'development') {
var _routing$router;
if (!routing || typeof window === 'undefined' || !('__NEXT_DATA__' in window)) {
return;
}
var isUsingNextRouter =
// @ts-expect-error: _isNextRouter is only set on the Next.js router
routing !== true && (routing === null || routing === void 0 ? void 0 : (_routing$router = routing.router) === null || _routing$router === void 0 ? void 0 : _routing$router._isNextRouter);
process.env.NODE_ENV === 'development' ? warn(isUsingNextRouter, "\nYou are using Next.js with InstantSearch without the \"react-instantsearch-router-nextjs\" package.\nThis package is recommended to make the routing work correctly with Next.js.\nPlease check its usage instructions: https://github.com/algolia/instantsearch/tree/master/packages/react-instantsearch-router-nextjs\n\nYou can ignore this warning if you are using a custom router that suits your needs, it won't be outputted in production builds.") : void 0;
}
}
function warnNextAppDir(isRscContextDefined) {
var _next;
if (!(process.env.NODE_ENV === 'development') || typeof window === 'undefined' || isRscContextDefined) {
return;
}
process.env.NODE_ENV === 'development' ? warn(Boolean((_next = window.next) === null || _next === void 0 ? void 0 : _next.appDir) === false, "\nWe've detected you are using Next.js with the App Router.\nWe released an **experimental** package called \"react-instantsearch-nextjs\" that makes SSR work with the App Router.\nPlease check its usage instructions: https://www.algolia.com/doc/guides/building-search-ui/going-further/server-side-rendering/react/#with-nextjs\n\nThis warning will not be outputted in production builds.") : void 0;
}
/**
* Gets the version of Next.js if it is available in the `window` object,
* otherwise it returns the NEXT_RUNTIME environment variable (in SSR),
* which is either `nodejs` or `edge`.
*/
function getNextVersion() {
var _next2, _process$env;
return typeof window !== 'undefined' && ((_next2 = window.next) === null || _next2 === void 0 ? void 0 : _next2.version) || (typeof process !== 'undefined' ? (_process$env = process.env) === null || _process$env === void 0 ? void 0 : _process$env.NEXT_RUNTIME : undefined);
}