Skip to content

Commit d146531

Browse files
committed
[Fizz] useEvent
1 parent cb5084d commit d146531

File tree

3 files changed

+115
-2
lines changed

3 files changed

+115
-2
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5545,4 +5545,101 @@ describe('ReactDOMFizzServer', () => {
55455545
expect(getVisibleChildren(container)).toEqual('Hi');
55465546
});
55475547
});
5548+
5549+
describe('useEvent', () => {
5550+
// @gate enableUseEventHook
5551+
it('can server render a component with useEvent', async () => {
5552+
const ref = React.createRef();
5553+
function App() {
5554+
const [count, setCount] = React.useState(0);
5555+
const onClick = React.experimental_useEvent(() => {
5556+
setCount(c => c + 1);
5557+
});
5558+
return (
5559+
<button ref={ref} onClick={() => onClick()}>
5560+
{count}
5561+
</button>
5562+
);
5563+
}
5564+
await act(async () => {
5565+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
5566+
pipe(writable);
5567+
});
5568+
expect(getVisibleChildren(container)).toEqual(<button>0</button>);
5569+
5570+
ReactDOMClient.hydrateRoot(container, <App />);
5571+
expect(Scheduler).toFlushAndYield([]);
5572+
expect(getVisibleChildren(container)).toEqual(<button>0</button>);
5573+
5574+
ref.current.dispatchEvent(
5575+
new window.MouseEvent('click', {bubbles: true}),
5576+
);
5577+
await jest.runAllTimers();
5578+
expect(getVisibleChildren(container)).toEqual(<button>1</button>);
5579+
});
5580+
5581+
// @gate enableUseEventHook
5582+
it('throws if useEvent is called during a server render', async () => {
5583+
const logs = [];
5584+
function App() {
5585+
const onRender = React.experimental_useEvent(() => {
5586+
logs.push('rendered');
5587+
});
5588+
onRender();
5589+
return <p>Hello</p>;
5590+
}
5591+
5592+
const reportedServerErrors = [];
5593+
let caughtError;
5594+
try {
5595+
await act(async () => {
5596+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />, {
5597+
onError(e) {
5598+
reportedServerErrors.push(e);
5599+
},
5600+
});
5601+
pipe(writable);
5602+
});
5603+
} catch (err) {
5604+
caughtError = err;
5605+
}
5606+
expect(logs).toEqual([]);
5607+
expect(caughtError.message).toContain(
5608+
'Cannot call a function returned by useEvent',
5609+
);
5610+
expect(reportedServerErrors).toEqual([caughtError]);
5611+
});
5612+
5613+
// @gate enableUseEventHook
5614+
it('does not guarantee useEvent return values during server rendering are distinct', async () => {
5615+
function App() {
5616+
const onClick1 = React.experimental_useEvent(() => {});
5617+
const onClick2 = React.experimental_useEvent(() => {});
5618+
return <div>{String(onClick1 === onClick2)}</div>;
5619+
}
5620+
await act(async () => {
5621+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
5622+
pipe(writable);
5623+
});
5624+
expect(getVisibleChildren(container)).toEqual(<div>true</div>);
5625+
5626+
const errors = [];
5627+
ReactDOMClient.hydrateRoot(container, <App />, {
5628+
onRecoverableError(error) {
5629+
errors.push(error);
5630+
},
5631+
});
5632+
expect(() => {
5633+
expect(Scheduler).toFlushAndYield([]);
5634+
}).toErrorDev(
5635+
[
5636+
'Text content did not match. Server: "true" Client: "false"',
5637+
'An error occurred during hydration',
5638+
],
5639+
{withoutStack: 1},
5640+
);
5641+
expect(errors.length).toEqual(2);
5642+
expect(getVisibleChildren(container)).toEqual(<div>false</div>);
5643+
});
5644+
});
55485645
});

packages/react-server/src/ReactFizzHooks.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {makeId} from './ReactServerFormatConfig';
3636
import {
3737
enableCache,
3838
enableUseHook,
39+
enableUseEventHook,
3940
enableUseMemoCacheHook,
4041
} from 'shared/ReactFeatureFlags';
4142
import is from 'shared/objectIs';
@@ -502,6 +503,16 @@ export function useCallback<T>(
502503
return useMemo(() => callback, deps);
503504
}
504505

506+
function throwOnUseEventCall() {
507+
throw new Error(
508+
'Cannot call a function returned by useEvent during server rendering.',
509+
);
510+
}
511+
512+
export function useEvent<T>(callback: () => T): () => T {
513+
return throwOnUseEventCall;
514+
}
515+
505516
// TODO Decide on how to implement this hook for server rendering.
506517
// If a mutation occurs during render, consider triggering a Suspense boundary
507518
// and falling back to client rendering.
@@ -657,6 +668,7 @@ export const Dispatcher: DispatcherType = {
657668
useInsertionEffect: noop,
658669
useLayoutEffect,
659670
useCallback,
671+
useEvent,
660672
// useImperativeHandle is not run in the server environment
661673
useImperativeHandle: noop,
662674
// Effects are not run in the server environment.
@@ -675,6 +687,9 @@ if (enableCache) {
675687
Dispatcher.getCacheForType = getCacheForType;
676688
Dispatcher.useCacheRefresh = useCacheRefresh;
677689
}
690+
if (enableUseEventHook) {
691+
Dispatcher.useEvent = useEvent;
692+
}
678693
if (enableUseMemoCacheHook) {
679694
Dispatcher.useMemoCache = useMemoCache;
680695
}

scripts/error-codes/codes.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -426,5 +426,6 @@
426426
"438": "An unsupported type was passed to use(): %s",
427427
"439": "We didn't expect to see a forward reference. This is a bug in the React Server.",
428428
"440": "An event from useEvent was called during render.",
429-
"441": "An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error."
430-
}
429+
"441": "An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.",
430+
"442": "Cannot call a function returned by useEvent during server rendering."
431+
}

0 commit comments

Comments
 (0)