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

Commit

Permalink
feat(getServerState): allow users to inject renderToString
Browse files Browse the repository at this point in the history
There are some cases where the combination of trying to make sure renderToString doesn't end up in a browser bundle, being runnable on esm + cjs, react 17 and 18, .js extension etc. blows up. One of those is pnpm caching removing "unused" packages.

This PR introduces a new argument `renderToString` to `getServerState` so you can inject the dependency yourself, meaning the import is within your own code and won't be purged.

```js
import { renderToString } from 'react-dom/server';
await getServerState(<App/>, renderToString);
await getServerState(<App/>, import('react-dom/server').then(mod => mod.renderToString));
```

fixes #3633
closes #3618
see vercel/next.js#40067
  • Loading branch information
Haroenv committed Oct 19, 2022
1 parent 596553c commit caa8022
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,20 @@ describe('ReactDOMServer imports', () => {
await expect(
getServerState(<App />)
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Could not import ReactDOMServer."`
`"Could not import ReactDOMServer. You can provide it as an argument: getServerState(<Search />, ReactDOMServer.renderToString)."`
);
});

test('does not throw if user provides their own renderToString', async () => {
const searchClient = createSearchClient({});
const { App } = createTestEnvironment({ searchClient });

const serverState = await getServerState(<App />, (element) =>
jest.requireActual('react-dom/server').renderToString(element)
);

expect(serverState.initialResults).toEqual(expect.any(Object));
});
});

function SearchBox() {
Expand Down
44 changes: 24 additions & 20 deletions packages/react-instantsearch-hooks-server/src/getServerState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@ import {
import type { InitialResults, InstantSearch, UiState } from 'instantsearch.js';
import type { IndexWidget } from 'instantsearch.js/es/widgets/index/index';
import type { ReactNode } from 'react';
import type { renderToString as reactRenderToString } from 'react-dom/server';
import type {
InstantSearchServerContextApi,
InstantSearchServerState,
} from 'react-instantsearch-hooks';

type SearchRef = { current: InstantSearch | undefined };

export type RenderToString = (element: JSX.Element) => unknown;

/**
* Returns the InstantSearch server state from a component.
*/
export function getServerState(
children: ReactNode
children: ReactNode,
renderToString?: RenderToString
): Promise<InstantSearchServerState> {
const searchRef: SearchRef = {
current: undefined,
Expand All @@ -33,16 +35,16 @@ export function getServerState(
searchRef.current = search;
};

return importRenderToString()
.then((renderToString) => {
return importRenderToString(renderToString)
.then((render) => {
return execute({
children,
renderToString,
render,
searchRef,
notifyServer,
}).then((serverState) => ({ serverState, renderToString }));
}).then((serverState) => ({ serverState, render }));
})
.then(({ renderToString, serverState }) => {
.then(({ render, serverState }) => {
let shouldRefetch = false;

// <DynamicWidgets> requires another query to retrieve the dynamic widgets
Expand All @@ -62,7 +64,7 @@ export function getServerState(
{children}
</InstantSearchSSRProvider>
),
renderToString,
render,
searchRef,
notifyServer,
});
Expand All @@ -74,18 +76,13 @@ export function getServerState(

type ExecuteArgs = {
children: ReactNode;
renderToString: typeof reactRenderToString;
render: RenderToString;
notifyServer: InstantSearchServerContextApi<UiState, UiState>['notifyServer'];
searchRef: SearchRef;
};

function execute({
children,
renderToString,
notifyServer,
searchRef,
}: ExecuteArgs) {
renderToString(
function execute({ children, render, notifyServer, searchRef }: ExecuteArgs) {
render(
<InstantSearchServerContext.Provider value={{ notifyServer }}>
{children}
</InstantSearchServerContext.Provider>
Expand Down Expand Up @@ -182,7 +179,13 @@ function getInitialResults(rootIndex: IndexWidget): InitialResults {
return initialResults;
}

function importRenderToString() {
function importRenderToString(
renderToString?: RenderToString
): Promise<RenderToString> {
if (renderToString) {
return Promise.resolve(renderToString);
}

// React pre-18 doesn't use `exports` in package.json, requiring a fully resolved path
// Thus, only one of these imports is correct
const modules = ['react-dom/server.js', 'react-dom/server'];
Expand All @@ -191,12 +194,13 @@ function importRenderToString() {
return Promise.all(modules.map((mod) => import(mod).catch(() => {}))).then(
(imports: unknown[]) => {
const ReactDOMServer = imports.find(
(mod): mod is { renderToString: typeof reactRenderToString } =>
mod !== undefined
(mod): mod is { renderToString: RenderToString } => mod !== undefined
);

if (!ReactDOMServer) {
throw new Error('Could not import ReactDOMServer.');
throw new Error(
'Could not import ReactDOMServer. You can provide it as an argument: getServerState(<Search />, ReactDOMServer.renderToString).'
);
}

return ReactDOMServer.renderToString;
Expand Down

0 comments on commit caa8022

Please sign in to comment.