diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 225d3657e0469..0ded4af8fd95b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -5545,4 +5545,101 @@ describe('ReactDOMFizzServer', () => { expect(getVisibleChildren(container)).toEqual('Hi'); }); }); + + describe('useEvent', () => { + // @gate enableUseEventHook + it('can server render a component with useEvent', async () => { + const ref = React.createRef(); + function App() { + const [count, setCount] = React.useState(0); + const onClick = React.experimental_useEvent(() => { + setCount(c => c + 1); + }); + return ( + + ); + } + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual(); + + ReactDOMClient.hydrateRoot(container, ); + expect(Scheduler).toFlushAndYield([]); + expect(getVisibleChildren(container)).toEqual(); + + ref.current.dispatchEvent( + new window.MouseEvent('click', {bubbles: true}), + ); + await jest.runAllTimers(); + expect(getVisibleChildren(container)).toEqual(); + }); + + // @gate enableUseEventHook + it('throws if useEvent is called during a server render', async () => { + const logs = []; + function App() { + const onRender = React.experimental_useEvent(() => { + logs.push('rendered'); + }); + onRender(); + return

Hello

; + } + + const reportedServerErrors = []; + let caughtError; + try { + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(, { + onError(e) { + reportedServerErrors.push(e); + }, + }); + pipe(writable); + }); + } catch (err) { + caughtError = err; + } + expect(logs).toEqual([]); + expect(caughtError.message).toContain( + 'Cannot call a function returned by useEvent', + ); + expect(reportedServerErrors).toEqual([caughtError]); + }); + + // @gate enableUseEventHook + it('does not guarantee useEvent return values during server rendering are distinct', async () => { + function App() { + const onClick1 = React.experimental_useEvent(() => {}); + const onClick2 = React.experimental_useEvent(() => {}); + return
{String(onClick1 === onClick2)}
; + } + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual(
true
); + + const errors = []; + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + errors.push(error); + }, + }); + expect(() => { + expect(Scheduler).toFlushAndYield([]); + }).toErrorDev( + [ + 'Text content did not match. Server: "true" Client: "false"', + 'An error occurred during hydration', + ], + {withoutStack: 1}, + ); + expect(errors.length).toEqual(2); + expect(getVisibleChildren(container)).toEqual(
false
); + }); + }); }); diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js index f6c8847a81b2d..6e77d5120d639 100644 --- a/packages/react-server/src/ReactFizzHooks.js +++ b/packages/react-server/src/ReactFizzHooks.js @@ -36,6 +36,7 @@ import {makeId} from './ReactServerFormatConfig'; import { enableCache, enableUseHook, + enableUseEventHook, enableUseMemoCacheHook, } from 'shared/ReactFeatureFlags'; import is from 'shared/objectIs'; @@ -502,6 +503,16 @@ export function useCallback( return useMemo(() => callback, deps); } +function throwOnUseEventCall() { + throw new Error( + 'Cannot call a function returned by useEvent during server rendering.', + ); +} + +export function useEvent(callback: () => T): () => T { + return throwOnUseEventCall; +} + // TODO Decide on how to implement this hook for server rendering. // If a mutation occurs during render, consider triggering a Suspense boundary // and falling back to client rendering. @@ -657,6 +668,7 @@ export const Dispatcher: DispatcherType = { useInsertionEffect: noop, useLayoutEffect, useCallback, + useEvent, // useImperativeHandle is not run in the server environment useImperativeHandle: noop, // Effects are not run in the server environment. @@ -675,6 +687,9 @@ if (enableCache) { Dispatcher.getCacheForType = getCacheForType; Dispatcher.useCacheRefresh = useCacheRefresh; } +if (enableUseEventHook) { + Dispatcher.useEvent = useEvent; +} if (enableUseMemoCacheHook) { Dispatcher.useMemoCache = useMemoCache; } diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 4b50cbdcfcf44..605395dd44763 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -426,5 +426,6 @@ "438": "An unsupported type was passed to use(): %s", "439": "We didn't expect to see a forward reference. This is a bug in the React Server.", "440": "An event from useEvent was called during render.", - "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." -} + "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.", + "442": "Cannot call a function returned by useEvent during server rendering." +} \ No newline at end of file