Skip to content

Commit

Permalink
feat(getServerState): allow users to inject renderToString (algolia/r…
Browse files Browse the repository at this point in the history
…eact-instantsearch#3658)

**Summary**

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/vercel removing "unused" packages.




<!--
  Thanks for submitting a pull request!
Please provide enough information so that others can review your pull
request.
-->


<!--
  Explain the **motivation** for making this change.
  What existing problem does the pull request solve?
  Are there any linked issues?
-->

**Result**


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));
```

<!--
  Demonstrate the code is solid.
  Example: The exact commands you ran and their output,
  screenshots / videos if the pull request changes UI.
-->

fixes algolia/react-instantsearch#3633
closes algolia/react-instantsearch#3618
see vercel/next.jsalgolia/react-instantsearch#40067
FX-1869

Co-authored-by: François Chalifour <francoischalifour@users.noreply.github.com>
  • Loading branch information
Haroenv and francoischalifour committed Oct 20, 2022
1 parent c7f34e1 commit cce66c5
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,25 @@ 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 />, { renderToString })."`
);
});

test('calls the provided renderToString function', async () => {
const searchClient = createSearchClient({});
const { App } = createTestEnvironment({ searchClient });

const renderToString = jest.fn(
jest.requireActual('react-dom/server').renderToString
);

const serverState = await getServerState(<App />, {
renderToString,
});

expect(renderToString).toHaveBeenCalledTimes(1);
expect(serverState.initialResults).toEqual(expect.any(Object));
});
});

function SearchBox() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,17 @@ jest.mock(

describe('ReactDOMServer imports', () => {
test('works when the import with an extension exists', async () => {
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
const searchClient = createSearchClient({});
const { App } = createTestEnvironment({ searchClient });

const serverState = await getServerState(<App />);

expect(serverState.initialResults).toEqual(expect.any(Object));
expect(warn).toHaveBeenCalledTimes(1);
expect(warn).toHaveBeenLastCalledWith(
'[InstantSearch] `renderToString` should be passed to getServerState(<App/>, { renderToString })'
);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,17 @@ jest.mock(

describe('ReactDOMServer imports', () => {
test('works when the import with no extension exists', async () => {
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
const searchClient = createSearchClient({});
const { App } = createTestEnvironment({ searchClient });

const serverState = await getServerState(<App />);

expect(serverState.initialResults).toEqual(expect.any(Object));
expect(warn).toHaveBeenCalledTimes(1);
expect(warn).toHaveBeenLastCalledWith(
'[InstantSearch] `renderToString` should be passed to getServerState(<App/>, { renderToString })'
);
});
});

Expand Down
33 changes: 25 additions & 8 deletions packages/react-instantsearch-hooks-server/src/getServerState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,25 @@ 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;

export type GetServerStateOptions = {
renderToString?: RenderToString;
};

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

return importRenderToString()
return importRenderToString(options.renderToString)
.then((renderToString) => {
return execute({
children,
Expand Down Expand Up @@ -74,7 +80,7 @@ export function getServerState(

type ExecuteArgs = {
children: ReactNode;
renderToString: typeof reactRenderToString;
renderToString: RenderToString;
notifyServer: InstantSearchServerContextApi<UiState, UiState>['notifyServer'];
searchRef: SearchRef;
};
Expand Down Expand Up @@ -182,7 +188,17 @@ function getInitialResults(rootIndex: IndexWidget): InitialResults {
return initialResults;
}

function importRenderToString() {
function importRenderToString(
renderToString?: RenderToString
): Promise<RenderToString> {
if (renderToString) {
return Promise.resolve(renderToString);
}
// eslint-disable-next-line no-console
console.warn(
'[InstantSearch] `renderToString` should be passed to getServerState(<App/>, { 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 +207,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 />, { renderToString }).'
);
}

return ReactDOMServer.renderToString;
Expand Down

0 comments on commit cce66c5

Please sign in to comment.