Skip to content

Commit

Permalink
refactor: move as much logic in new package as possible + dynamicWidg…
Browse files Browse the repository at this point in the history
…ets support
  • Loading branch information
aymeric-giraudet committed Sep 4, 2023
1 parent cb29242 commit 664bc7d
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 77 deletions.
6 changes: 0 additions & 6 deletions examples/react/next-app-router/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,6 @@ export default function RootLayout({
}) {
return (
<html lang="en">
<head>
<meta
name="google-site-verification"
content="AMB0fkgxwBoeRwRG93xQ7IUkmteaFHss6Jz9BBvq7EU"
/>
</head>
<body>{children}</body>
</html>
);
Expand Down
13 changes: 12 additions & 1 deletion examples/react/next-app-router/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import {
Highlight,
SearchBox,
RefinementList,
DynamicWidgets,
} from 'react-instantsearch';
import { NextInstantSearchSSR } from 'react-instantsearch-ssr-nextjs';

import { Panel } from '../components/Panel';

const client = algoliasearch('latency', '6be0576ff61c053d5f9a3225e2a90f76');

type HitProps = {
Expand All @@ -34,7 +37,7 @@ export default function SearchPage() {
<NextInstantSearchSSR searchClient={client} indexName="instant_search">
<div className="Container">
<div>
<RefinementList attribute="brand" />
<DynamicWidgets fallbackComponent={FallbackComponent} />
</div>
<div>
<SearchBox />
Expand All @@ -44,3 +47,11 @@ export default function SearchPage() {
</NextInstantSearchSSR>
);
}

function FallbackComponent({ attribute }: { attribute: string }) {
return (
<Panel header={attribute}>
<RefinementList attribute={attribute} />
</Panel>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,8 @@ import { createContext } from 'react';
import type { PromiseWithState } from './wrapPromiseWithState';
import type { MutableRefObject } from 'react';

export type InstantSearchRSCContextApi = {
promiseRef: MutableRefObject<PromiseWithState<void> | null>;
insertHTML: (callbacks: () => React.ReactNode) => void;
};
export type InstantSearchRSCContextApi =
MutableRefObject<PromiseWithState<void> | null> | null;

export const InstantSearchRSCContext =
createContext<InstantSearchRSCContextApi>({
promiseRef: { current: null },
insertHTML: () => {},
});
createContext<InstantSearchRSCContextApi>(null);
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -55,30 +51,18 @@ export type InternalInstantSearch<
_preventWidgetCleanup?: boolean;
};

const InstantSearchInitialResults = Symbol.for('InstantSearchInitialResults');
declare global {
interface Window {
[InstantSearchInitialResults]?: InitialResults;
}
}

export function useInstantSearchApi<TUiState extends UiState, TRouteState>(
props: UseInstantSearchApiProps<TUiState, TRouteState>
) {
const forceUpdate = useForceUpdate();
const serverContext = useInstantSearchServerContext<TUiState, TRouteState>();
const serverState = useInstantSearchSSRContext<TUiState, TRouteState>();
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<InternalInstantSearch<TUiState, TRouteState> | null>(
null
Expand Down Expand Up @@ -112,7 +96,7 @@ export function useInstantSearchApi<TUiState extends UiState, TRouteState>(
} 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
Expand All @@ -131,7 +115,7 @@ export function useInstantSearchApi<TUiState extends UiState, TRouteState>(
// 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();
}

Expand All @@ -141,26 +125,6 @@ export function useInstantSearchApi<TUiState extends UiState, TRouteState>(
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(() => (
<script
dangerouslySetInnerHTML={{
__html: `(window[Symbol.for("InstantSearchInitialResults")] ??= []).push(${JSON.stringify(
results
)})`,
}}
/>
));
resolve();
});
})
);
}

warnNextRouter(props.routing);

searchRef.current = search;
Expand Down
11 changes: 7 additions & 4 deletions packages/react-instantsearch-core/src/lib/useWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function useWidget<TWidget extends Widget | IndexWidget, TProps>({
props: TProps;
shouldSsr: boolean;
}) {
const { promiseRef } = useRSCContext();
const waitingForResultsRef = useRSCContext();

const prevPropsRef = useRef<TProps>(props);
useEffect(() => {
Expand Down Expand Up @@ -87,11 +87,14 @@ export function useWidget<TWidget extends Widget | IndexWidget, TProps>({
};
}, [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);
}
}
103 changes: 90 additions & 13 deletions packages/react-instantsearch-ssr-nextjs/src/NextInstantSearchSSR.tsx
Original file line number Diff line number Diff line change
@@ -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<TUiState, TRouteState>;

export function NextInstantSearchSSR<
Expand All @@ -29,27 +40,93 @@ export function NextInstantSearchSSR<
...instantSearchProps
}: NextInstantSearchSSRProps<TUiState, TRouteState>) {
const promiseRef = useRef<PromiseWithState<void> | null>(null);
const isServerSide = typeof window === 'undefined';

let initialResults;
if (!isServerSide) {
initialResults = window[InstantSearchInitialResults]?.pop();
}

return (
<InstantSearchRSCContext.Provider value={promiseRef}>
<InstantSearchSSRProvider initialResults={initialResults}>
<InstantSearch {...instantSearchProps}>
{isServerSide && <InitializePromise />}
{children}
{isServerSide && <TriggerSearch />}
</InstantSearch>
</InstantSearchSSRProvider>
</InstantSearchRSCContext.Provider>
);
}

function InitializePromise() {
const search = useInstantSearchContext();
const waitForResultsRef = useRSCContext();
const insertHTML =
useContext(ServerInsertedHTMLContext) ||
(() => {
throw new Error('Missing ServerInsertedHTMLContext');
});

return (
<InstantSearchRSCContext.Provider value={{ promiseRef, insertHTML }}>
<InstantSearch {...instantSearchProps}>
{children}
<TriggerSearch />
</InstantSearch>
</InstantSearchRSCContext.Provider>
);
const injectInitialResults = () => {
const results = getInitialResults(search.mainIndex);
insertHTML(() => (
<script
dangerouslySetInnerHTML={{
__html: `(window[Symbol.for("InstantSearchInitialResults")] ??= []).push(${JSON.stringify(
results
)})`,
}}
/>
));
};

if (waitForResultsRef?.current === null) {
waitForResultsRef.current = wrapPromiseWithState(
new Promise<boolean>((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<void>((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();
}

Expand Down

0 comments on commit 664bc7d

Please sign in to comment.