@@ -44,3 +47,11 @@ export default function SearchPage() {
);
}
+
+function FallbackComponent({ attribute }: { attribute: string }) {
+ return (
+
+
+
+ );
+}
diff --git a/packages/react-instantsearch-core/src/lib/InstantSearchRSCContext.ts b/packages/react-instantsearch-core/src/lib/InstantSearchRSCContext.ts
index 82b84d0a356..851b46bf039 100644
--- a/packages/react-instantsearch-core/src/lib/InstantSearchRSCContext.ts
+++ b/packages/react-instantsearch-core/src/lib/InstantSearchRSCContext.ts
@@ -3,13 +3,8 @@ import { createContext } from 'react';
import type { PromiseWithState } from './wrapPromiseWithState';
import type { MutableRefObject } from 'react';
-export type InstantSearchRSCContextApi = {
- promiseRef: MutableRefObject
| null>;
- insertHTML: (callbacks: () => React.ReactNode) => void;
-};
+export type InstantSearchRSCContextApi =
+ MutableRefObject | null> | null;
export const InstantSearchRSCContext =
- createContext({
- promiseRef: { current: null },
- insertHTML: () => {},
- });
+ createContext(null);
diff --git a/packages/react-instantsearch-core/src/lib/useInstantSearchApi.tsx b/packages/react-instantsearch-core/src/lib/useInstantSearchApi.ts
similarity index 85%
rename from packages/react-instantsearch-core/src/lib/useInstantSearchApi.tsx
rename to packages/react-instantsearch-core/src/lib/useInstantSearchApi.ts
index dde61b98f7e..8810839a59e 100644
--- a/packages/react-instantsearch-core/src/lib/useInstantSearchApi.tsx
+++ b/packages/react-instantsearch-core/src/lib/useInstantSearchApi.ts
@@ -1,20 +1,16 @@
-/* eslint-disable complexity */
import InstantSearch from 'instantsearch.js/es/lib/InstantSearch';
-import { getInitialResults } from 'instantsearch.js/es/lib/server';
-import React, { useCallback, useRef, version as ReactVersion } from 'react';
+import { useCallback, useRef, version as ReactVersion } from 'react';
import { useSyncExternalStore } from 'use-sync-external-store/shim';
-import { useInstantSearchServerContext } from '../lib/useInstantSearchServerContext';
-import { useInstantSearchSSRContext } from '../lib/useInstantSearchSSRContext';
import version from '../version';
import { useForceUpdate } from './useForceUpdate';
+import { useInstantSearchServerContext } from './useInstantSearchServerContext';
+import { useInstantSearchSSRContext } from './useInstantSearchSSRContext';
import { useRSCContext } from './useRSCContext';
import { warn } from './warn';
-import { wrapPromiseWithState } from './wrapPromiseWithState';
import type {
- InitialResults,
InstantSearchOptions,
SearchClient,
UiState,
@@ -55,30 +51,18 @@ export type InternalInstantSearch<
_preventWidgetCleanup?: boolean;
};
-const InstantSearchInitialResults = Symbol.for('InstantSearchInitialResults');
-declare global {
- interface Window {
- [InstantSearchInitialResults]?: InitialResults;
- }
-}
-
export function useInstantSearchApi(
props: UseInstantSearchApiProps
) {
const forceUpdate = useForceUpdate();
const serverContext = useInstantSearchServerContext();
const serverState = useInstantSearchSSRContext();
- const { promiseRef, insertHTML } = useRSCContext();
- let initialResults =
- serverState?.initialResults ||
- (typeof window !== 'undefined'
- ? window[InstantSearchInitialResults]
- : undefined);
+ const waitingForResultsRef = useRSCContext();
+ const initialResults = serverState?.initialResults;
const prevPropsRef = useRef(props);
- if (Array.isArray(initialResults)) {
- initialResults = initialResults.pop();
- }
+ const shouldRenderAtOnce =
+ serverContext || initialResults || waitingForResultsRef;
let searchRef = useRef | null>(
null
@@ -112,7 +96,7 @@ export function useInstantSearchApi(
} as typeof search._schedule;
search._schedule.queue = [];
- if (serverContext || initialResults) {
+ 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
@@ -131,7 +115,7 @@ export function useInstantSearchApi(
// 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 (serverContext || initialResults || promiseRef?.current === null) {
+ if (shouldRenderAtOnce) {
search.start();
}
@@ -141,26 +125,6 @@ export function useInstantSearchApi(
serverContext.notifyServer({ search });
}
- if (promiseRef?.current === null && typeof window === 'undefined') {
- promiseRef.current = wrapPromiseWithState(
- new Promise((resolve) => {
- search.once('render', () => {
- const results = getInitialResults(search.mainIndex);
- insertHTML(() => (
-
- ));
- resolve();
- });
- })
- );
- }
-
warnNextRouter(props.routing);
searchRef.current = search;
diff --git a/packages/react-instantsearch-core/src/lib/useWidget.ts b/packages/react-instantsearch-core/src/lib/useWidget.ts
index 62a9707869e..45b59740752 100644
--- a/packages/react-instantsearch-core/src/lib/useWidget.ts
+++ b/packages/react-instantsearch-core/src/lib/useWidget.ts
@@ -20,7 +20,7 @@ export function useWidget({
props: TProps;
shouldSsr: boolean;
}) {
- const { promiseRef } = useRSCContext();
+ const waitingForResultsRef = useRSCContext();
const prevPropsRef = useRef(props);
useEffect(() => {
@@ -87,11 +87,14 @@ export function useWidget({
};
}, [parentIndex, widget, shouldAddWidgetEarly, search, props]);
- if (shouldAddWidgetEarly && promiseRef.current?.status === 'pending') {
+ if (
+ shouldAddWidgetEarly ||
+ waitingForResultsRef?.current?.status === 'pending'
+ ) {
parentIndex.addWidgets([widget]);
}
- if (typeof window === 'undefined' && promiseRef.current) {
- __use(promiseRef.current);
+ if (typeof window === 'undefined' && waitingForResultsRef?.current) {
+ __use(waitingForResultsRef.current);
}
}
diff --git a/packages/react-instantsearch-ssr-nextjs/src/NextInstantSearchSSR.tsx b/packages/react-instantsearch-ssr-nextjs/src/NextInstantSearchSSR.tsx
index 12ec9e96ca4..fbf21a8f275 100644
--- a/packages/react-instantsearch-ssr-nextjs/src/NextInstantSearchSSR.tsx
+++ b/packages/react-instantsearch-ssr-nextjs/src/NextInstantSearchSSR.tsx
@@ -1,24 +1,35 @@
+import { getInitialResults } from 'instantsearch.js/es/lib/server';
+import { walkIndex } from 'instantsearch.js/es/lib/utils';
import { ServerInsertedHTMLContext } from 'next/navigation';
import React, { useContext, useRef } from 'react';
import {
InstantSearch,
InstantSearchRSCContext,
+ InstantSearchSSRProvider,
useInstantSearchContext,
useRSCContext,
+ wrapPromiseWithState,
} from 'react-instantsearch-core';
-import type { UiState } from 'instantsearch.js';
-import type { ReactElement } from 'react';
+import type { InitialResults, UiState } from 'instantsearch.js';
+import type { ReactNode } from 'react';
import type {
InstantSearchProps,
PromiseWithState,
} from 'react-instantsearch-core';
+const InstantSearchInitialResults = Symbol.for('InstantSearchInitialResults');
+declare global {
+ interface Window {
+ [InstantSearchInitialResults]?: InitialResults[];
+ }
+}
+
export type NextInstantSearchSSRProps<
TUiState extends UiState = UiState,
TRouteState = TUiState
> = {
- children: ReactElement;
+ children: ReactNode;
} & InstantSearchProps;
export function NextInstantSearchSSR<
@@ -29,27 +40,93 @@ export function NextInstantSearchSSR<
...instantSearchProps
}: NextInstantSearchSSRProps) {
const promiseRef = useRef | null>(null);
+ const isServerSide = typeof window === 'undefined';
+
+ let initialResults;
+ if (!isServerSide) {
+ initialResults = window[InstantSearchInitialResults]?.pop();
+ }
+
+ return (
+
+
+
+ {isServerSide && }
+ {children}
+ {isServerSide && }
+
+
+
+ );
+}
+
+function InitializePromise() {
+ const search = useInstantSearchContext();
+ const waitForResultsRef = useRSCContext();
const insertHTML =
useContext(ServerInsertedHTMLContext) ||
(() => {
throw new Error('Missing ServerInsertedHTMLContext');
});
- return (
-
-
- {children}
-
-
-
- );
+ const injectInitialResults = () => {
+ const results = getInitialResults(search.mainIndex);
+ insertHTML(() => (
+
+ ));
+ };
+
+ if (waitForResultsRef?.current === null) {
+ waitForResultsRef.current = wrapPromiseWithState(
+ new Promise((onFirstResponse) => {
+ search.once('render', () => {
+ let shouldRefetch = false;
+
+ walkIndex(search.mainIndex, (index) => {
+ shouldRefetch = index
+ .getWidgets()
+ .some((widget) => widget.$$type === 'ais.dynamicWidgets');
+ });
+
+ if (shouldRefetch) {
+ waitForResultsRef.current = wrapPromiseWithState(
+ new Promise((onSecondResponse) => {
+ // We need to wait for 2 renders, one for the dynamic widgets to be rendered
+ // and one for the results to be fetched
+ search.once('render', () => {
+ search.once('render', () => {
+ onSecondResponse();
+ });
+ });
+ }).then(injectInitialResults)
+ );
+ }
+
+ onFirstResponse(shouldRefetch);
+ });
+ }).then((shouldRefetch) => {
+ if (shouldRefetch) {
+ return;
+ }
+ injectInitialResults();
+ })
+ );
+ }
+
+ return null;
}
function TriggerSearch() {
const instantsearch = useInstantSearchContext();
- const { promiseRef } = useRSCContext();
+ const waitForResultsRef = useRSCContext();
- if (promiseRef.current?.status === 'pending') {
+ if (waitForResultsRef?.current?.status === 'pending') {
instantsearch.mainHelper?.searchOnlyWithDerivedHelpers();
}