Skip to content

Commit bcb66d3

Browse files
100terresklarstrup
andauthoredNov 26, 2022
fix(ssr): use useId for hydration and remove the need for resetServerContext (React 18+) (#439)
* feat(ssr): remove need for resetServerContext when useId is available (i.e. React 18+) * refactor: add deprecated prefix to some functions (with test utils) * test(process.env): add missing types for the csp-server Co-authored-by: klarstrup <io@klarstrup.dk>
1 parent 3e33920 commit bcb66d3

12 files changed

+108
-36
lines changed
 

‎.size-snapshot.json

+11-11
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
{
22
"dnd.js": {
3-
"bundled": 312882,
4-
"minified": 119680,
5-
"gzipped": 36562
3+
"bundled": 313453,
4+
"minified": 119985,
5+
"gzipped": 36652
66
},
77
"dnd.min.js": {
8-
"bundled": 275071,
9-
"minified": 101956,
10-
"gzipped": 30151
8+
"bundled": 275553,
9+
"minified": 102189,
10+
"gzipped": 30211
1111
},
1212
"dnd.esm.js": {
13-
"bundled": 217232,
14-
"minified": 121547,
15-
"gzipped": 32474,
13+
"bundled": 217729,
14+
"minified": 121864,
15+
"gzipped": 32558,
1616
"treeshaked": {
1717
"rollup": {
18-
"code": 18507,
18+
"code": 18649,
1919
"import_statements": 456
2020
},
2121
"webpack": {
22-
"code": 21510
22+
"code": 21689
2323
}
2424
}
2525
}

‎csp-server/environment.d.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
interface ProcessEnv {
2+
NODE_ENV?: 'development' | 'production';
3+
REACT_MAJOR_VERSION?: '16' | '17' | '18';
4+
CI?: boolean;
5+
}
6+
7+
interface Process {
8+
env: ProcessEnv;
9+
}

‎csp-server/server.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ import { renderToString } from 'react-dom/server';
44
import { resetServerContext } from '@hello-pangea/dnd';
55
import { resolve } from 'path';
66
import App from './app';
7+
import invokeOnReactVersion from '../test/util/invoke-on-react-version';
78

89
let count = 0;
910
function getNonce(): string {
1011
return `ThisShouldBeACryptographicallySecurePseudoRandomNumber-${count++}`;
1112
}
1213

1314
function renderHtml(policy?: string, nonce?: string) {
14-
resetServerContext();
15+
invokeOnReactVersion(['16', '17'], resetServerContext);
16+
1517
let meta = '';
1618
if (nonce) {
1719
meta += `<meta id="csp-nonce" property="csp-nonce" content="${nonce}" />`;
@@ -32,6 +34,7 @@ export default (port: string, outputPath: string, fs: any): void => {
3234
res.end(fs.readFileSync(resolve(outputPath, 'client.js')));
3335
});
3436

37+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3538
function render(res: any, policy?: string, nonce?: string) {
3639
if (policy) {
3740
res.header('Content-Security-Policy', policy);

‎jest.config.ts

+6
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ if (['16', '17'].includes(reactMajorVersion)) {
5252
'^react-dom((\\/.*)?)$': `react-dom-${reactMajorVersion}$1`,
5353
'^react((\\/.*)?)$': `react-${reactMajorVersion}$1`,
5454
};
55+
} else {
56+
config.testPathIgnorePatterns = [
57+
...(config.testPathIgnorePatterns || []),
58+
// resetServerContext is irrelevant from 18 onwards
59+
'test/unit/integration/drag-drop-context/reset-server-context.spec.tsx',
60+
];
5561
}
5662

5763
if (isRunningInCI()) {

‎src/view/drag-drop-context/drag-drop-context.tsx

+14-4
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import React from 'react';
22
import type { ReactNode } from 'react';
33
import type { Responders, ContextId, Sensor } from '../../types';
44
import ErrorBoundary from './error-boundary';
5+
import { warning } from '../../dev-warning';
56
import preset from '../../screen-reader-message-preset';
67
import App from './app';
78
import useUniqueContextId, {
8-
reset as resetContextId,
9+
resetDeprecatedUniqueContextId,
910
} from './use-unique-context-id';
10-
import { reset as resetUniqueIds } from '../use-unique-id';
11+
import { resetDeprecatedUniqueId } from '../use-unique-id';
1112
import { PartialAutoScrollerOptions } from '../../state/auto-scroller/fluid-scroller/auto-scroller-options-types';
1213

1314
export interface DragDropContextProps extends Responders {
@@ -29,8 +30,17 @@ export interface DragDropContextProps extends Responders {
2930

3031
// Reset any context that gets persisted across server side renders
3132
export function resetServerContext() {
32-
resetContextId();
33-
resetUniqueIds();
33+
// The useId hook is only available in React 18+
34+
if ('useId' in React) {
35+
warning(
36+
`It is not necessary to call resetServerContext when using React 18+`,
37+
);
38+
39+
return;
40+
}
41+
42+
resetDeprecatedUniqueContextId();
43+
resetDeprecatedUniqueId();
3444
}
3545

3646
export default function DragDropContext(props: DragDropContextProps) {
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
1+
import React from 'react';
12
import { useMemo } from 'use-memo-one';
23
import type { ContextId } from '../../types';
34

45
let count = 0;
56

6-
export function reset() {
7+
export function resetDeprecatedUniqueContextId() {
78
count = 0;
89
}
910

10-
export default function useInstanceCount(): ContextId {
11+
function useDeprecatedUniqueContextId(): ContextId {
1112
return useMemo(() => `${count++}`, []);
1213
}
14+
15+
function useUniqueContextId(): ContextId {
16+
return React.useId();
17+
}
18+
19+
// The useId hook is only available in React 18+
20+
export default 'useId' in React
21+
? useUniqueContextId
22+
: useDeprecatedUniqueContextId;

‎src/view/use-unique-id.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import React from 'react';
12
import { useMemo } from 'use-memo-one';
23
import type { Id } from '../types';
34

@@ -9,11 +10,11 @@ interface Options {
910

1011
const defaults: Options = { separator: '::' };
1112

12-
export function reset() {
13+
export function resetDeprecatedUniqueId() {
1314
count = 0;
1415
}
1516

16-
export default function useUniqueId(
17+
function useDeprecatedUniqueId(
1718
prefix: string,
1819
options: Options = defaults,
1920
): Id {
@@ -22,3 +23,15 @@ export default function useUniqueId(
2223
[options.separator, prefix],
2324
);
2425
}
26+
27+
function useUniqueId(prefix: string, options: Options = defaults): Id {
28+
const id = React.useId();
29+
30+
return useMemo(
31+
() => `${prefix}${options.separator}${id}`,
32+
[options.separator, prefix, id],
33+
);
34+
}
35+
36+
// The useId hook is only available in React 18+
37+
export default 'useId' in React ? useUniqueId : useDeprecatedUniqueId;
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`should support rendering to a string 1`] = `"<main><div data-rfd-droppable-id="droppable" data-rfd-droppable-context-id="0"><div data-rfd-draggable-context-id="0" data-rfd-draggable-id="0" tabindex="0" role="button" aria-describedby="rfd-hidden-text-0-hidden-text-0" data-rfd-drag-handle-draggable-id="0" data-rfd-drag-handle-context-id="0" draggable="false" data-is-dragging="false" data-is-drop-animating="false" data-is-combining="false" data-is-combine-target="false" data-is-clone="false" data-testid="0">item: <!-- -->0</div><div data-rfd-draggable-context-id="0" data-rfd-draggable-id="1" tabindex="0" role="button" aria-describedby="rfd-hidden-text-0-hidden-text-0" data-rfd-drag-handle-draggable-id="1" data-rfd-drag-handle-context-id="0" draggable="false" data-is-dragging="false" data-is-drop-animating="false" data-is-combining="false" data-is-combine-target="false" data-is-clone="false" data-testid="1">item: <!-- -->1</div><div data-rfd-draggable-context-id="0" data-rfd-draggable-id="2" tabindex="0" role="button" aria-describedby="rfd-hidden-text-0-hidden-text-0" data-rfd-drag-handle-draggable-id="2" data-rfd-drag-handle-context-id="0" draggable="false" data-is-dragging="false" data-is-drop-animating="false" data-is-combining="false" data-is-combine-target="false" data-is-clone="false" data-testid="2">item: <!-- -->2</div></div></main>"`;
3+
exports[`should support rendering to a string 1`] = `"<main><div data-rfd-droppable-id="droppable" data-rfd-droppable-context-id=":R0:"><div data-rfd-draggable-context-id=":R0:" data-rfd-draggable-id="0" tabindex="0" role="button" aria-describedby="rfd-hidden-text-:R0:-hidden-text-:R1:" data-rfd-drag-handle-draggable-id="0" data-rfd-drag-handle-context-id=":R0:" draggable="false" data-is-dragging="false" data-is-drop-animating="false" data-is-combining="false" data-is-combine-target="false" data-is-clone="false" data-testid="0">item: <!-- -->0</div><div data-rfd-draggable-context-id=":R0:" data-rfd-draggable-id="1" tabindex="0" role="button" aria-describedby="rfd-hidden-text-:R0:-hidden-text-:R1:" data-rfd-drag-handle-draggable-id="1" data-rfd-drag-handle-context-id=":R0:" draggable="false" data-is-dragging="false" data-is-drop-animating="false" data-is-combining="false" data-is-combine-target="false" data-is-clone="false" data-testid="1">item: <!-- -->1</div><div data-rfd-draggable-context-id=":R0:" data-rfd-draggable-id="2" tabindex="0" role="button" aria-describedby="rfd-hidden-text-:R0:-hidden-text-:R1:" data-rfd-drag-handle-draggable-id="2" data-rfd-drag-handle-context-id=":R0:" draggable="false" data-is-dragging="false" data-is-drop-animating="false" data-is-combining="false" data-is-combine-target="false" data-is-clone="false" data-testid="2">item: <!-- -->2</div></div></main>"`;
44

5-
exports[`should support rendering to static markup 1`] = `"<main><div data-rfd-droppable-id="droppable" data-rfd-droppable-context-id="0"><div data-rfd-draggable-context-id="0" data-rfd-draggable-id="0" tabindex="0" role="button" aria-describedby="rfd-hidden-text-0-hidden-text-0" data-rfd-drag-handle-draggable-id="0" data-rfd-drag-handle-context-id="0" draggable="false" data-is-dragging="false" data-is-drop-animating="false" data-is-combining="false" data-is-combine-target="false" data-is-clone="false" data-testid="0">item: 0</div><div data-rfd-draggable-context-id="0" data-rfd-draggable-id="1" tabindex="0" role="button" aria-describedby="rfd-hidden-text-0-hidden-text-0" data-rfd-drag-handle-draggable-id="1" data-rfd-drag-handle-context-id="0" draggable="false" data-is-dragging="false" data-is-drop-animating="false" data-is-combining="false" data-is-combine-target="false" data-is-clone="false" data-testid="1">item: 1</div><div data-rfd-draggable-context-id="0" data-rfd-draggable-id="2" tabindex="0" role="button" aria-describedby="rfd-hidden-text-0-hidden-text-0" data-rfd-drag-handle-draggable-id="2" data-rfd-drag-handle-context-id="0" draggable="false" data-is-dragging="false" data-is-drop-animating="false" data-is-combining="false" data-is-combine-target="false" data-is-clone="false" data-testid="2">item: 2</div></div></main>"`;
5+
exports[`should support rendering to static markup 1`] = `"<main><div data-rfd-droppable-id="droppable" data-rfd-droppable-context-id=":R0:"><div data-rfd-draggable-context-id=":R0:" data-rfd-draggable-id="0" tabindex="0" role="button" aria-describedby="rfd-hidden-text-:R0:-hidden-text-:R1:" data-rfd-drag-handle-draggable-id="0" data-rfd-drag-handle-context-id=":R0:" draggable="false" data-is-dragging="false" data-is-drop-animating="false" data-is-combining="false" data-is-combine-target="false" data-is-clone="false" data-testid="0">item: 0</div><div data-rfd-draggable-context-id=":R0:" data-rfd-draggable-id="1" tabindex="0" role="button" aria-describedby="rfd-hidden-text-:R0:-hidden-text-:R1:" data-rfd-drag-handle-draggable-id="1" data-rfd-drag-handle-context-id=":R0:" draggable="false" data-is-dragging="false" data-is-drop-animating="false" data-is-combining="false" data-is-combine-target="false" data-is-clone="false" data-testid="1">item: 1</div><div data-rfd-draggable-context-id=":R0:" data-rfd-draggable-id="2" tabindex="0" role="button" aria-describedby="rfd-hidden-text-:R0:-hidden-text-:R1:" data-rfd-drag-handle-draggable-id="2" data-rfd-drag-handle-context-id=":R0:" draggable="false" data-is-dragging="false" data-is-drop-animating="false" data-is-combining="false" data-is-combine-target="false" data-is-clone="false" data-testid="2">item: 2</div></div></main>"`;

‎test/unit/integration/server-side-rendering/client-hydration.spec.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import { resetServerContext } from '../../../../src';
66
import App from '../util/app';
77
import { noop } from '../../../../src/empty';
88
import getBodyElement from '../../../../src/view/get-body-element';
9+
import invokeOnReactVersion from '../../../util/invoke-on-react-version';
910

1011
beforeEach(() => {
1112
// Reset server context between tests to prevent state being shared between them
12-
resetServerContext();
13+
invokeOnReactVersion(['16', '17'], resetServerContext);
1314
});
1415

1516
// Checking that the browser globals are available in this test file
@@ -25,7 +26,7 @@ it('should support hydrating a server side rendered application', () => {
2526
// on the server
2627
const error = jest.spyOn(console, 'error').mockImplementation(noop);
2728

28-
resetServerContext();
29+
invokeOnReactVersion(['16', '17'], resetServerContext);
2930
const serverHTML: string = ReactDOMServer.renderToString(<App />);
3031

3132
error.mock.calls.forEach((call) => {
@@ -37,7 +38,7 @@ it('should support hydrating a server side rendered application', () => {
3738

3839
// would be done client side
3940
// would have a fresh server context on the client
40-
resetServerContext();
41+
invokeOnReactVersion(['16', '17'], resetServerContext);
4142
const el = document.createElement('div');
4243
el.innerHTML = serverHTML;
4344
getBodyElement().appendChild(el);

‎test/unit/integration/server-side-rendering/server-rendering.spec.tsx

+14-9
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ import { renderToString, renderToStaticMarkup } from 'react-dom/server';
66
import { invariant } from '../../../../src/invariant';
77
import { resetServerContext } from '../../../../src';
88
import App from '../util/app';
9+
import invokeOnReactVersion from '../../../util/invoke-on-react-version';
910

1011
let consoleWarnSpy: jest.SpiedFunction<typeof console.warn>;
1112
let consoleErrorSpy: jest.SpiedFunction<typeof console.error>;
1213
let consoleLogSpy: jest.SpiedFunction<typeof console.log>;
1314

1415
beforeEach(() => {
1516
// Reset server context between tests to prevent state being shared between them
16-
resetServerContext();
17+
invokeOnReactVersion(['16', '17'], resetServerContext);
1718

1819
consoleWarnSpy = jest.spyOn(console, 'warn');
1920
consoleErrorSpy = jest.spyOn(console, 'error');
@@ -54,13 +55,17 @@ it('should support rendering to static markup', () => {
5455
expectConsoleNotCalled();
5556
});
5657

57-
it('should render identical content when resetting context between renders', () => {
58-
const firstRender = renderToString(<App />);
59-
const nextRenderBeforeReset = renderToString(<App />);
60-
expect(firstRender).not.toEqual(nextRenderBeforeReset);
58+
// This test is unsuited for React 18+, which with useId can
59+
// produce simultaneously unique and deterministic IDs
60+
invokeOnReactVersion(['16', '17'], () => {
61+
it('should render identical content when resetting context between renders', () => {
62+
const firstRender = renderToString(<App />);
63+
const nextRenderBeforeReset = renderToString(<App />);
64+
expect(firstRender).not.toEqual(nextRenderBeforeReset);
6165

62-
resetServerContext();
63-
const nextRenderAfterReset = renderToString(<App />);
64-
expect(firstRender).toEqual(nextRenderAfterReset);
65-
expectConsoleNotCalled();
66+
resetServerContext();
67+
const nextRenderAfterReset = renderToString(<App />);
68+
expect(firstRender).toEqual(nextRenderAfterReset);
69+
expectConsoleNotCalled();
70+
});
6671
});

‎test/unit/view/drag-drop-context/content-security-protection-nonce.spec.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,22 @@ import React from 'react';
33
import DragDropContext from '../../../../src/view/drag-drop-context';
44
import { resetServerContext } from '../../../../src';
55
import * as attributes from '../../../../src/view/data-attributes';
6+
import getReactMajorVersion from '../../../util/get-react-major-version';
7+
import invokeOnReactVersion from '../../../util/invoke-on-react-version';
68

9+
const isReact18 = getReactMajorVersion() === '18';
710
it('should insert nonce into style tag', () => {
811
const nonce = 'ThisShouldBeACryptographicallySecurePseudorandomNumber';
912

10-
resetServerContext();
13+
invokeOnReactVersion(['16', '17'], resetServerContext);
1114
const { unmount } = render(
1215
<DragDropContext nonce={nonce} onDragEnd={() => {}}>
1316
{null}
1417
</DragDropContext>,
1518
);
16-
const styleTag = document.querySelector(`[${attributes.prefix}-always="0"]`);
19+
const styleTag = document.querySelector(
20+
`[${attributes.prefix}-always="${isReact18 ? ':r0:' : '0'}"]`,
21+
);
1722
const nonceAttribute = styleTag ? styleTag.getAttribute('nonce') : '';
1823
expect(nonceAttribute).toEqual(nonce);
1924

‎test/util/invoke-on-react-version.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import getReactMajorVersion from './get-react-major-version';
2+
3+
export default function invokeOnReactVersion(
4+
reactVersion: Array<'16' | '17' | '18'>,
5+
callback: () => void,
6+
) {
7+
if (reactVersion.includes(getReactMajorVersion())) {
8+
callback();
9+
}
10+
}

0 commit comments

Comments
 (0)