From 3de926449681015c4f1eec1147baa03a3cb823f5 Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 27 Sep 2022 20:42:16 +0100 Subject: [PATCH] [Fizz] experimental_useEvent (#25325) * [Fizz] useEvent * Use same message on client and server --- .../src/__tests__/ReactDOMFizzServer-test.js | 101 ++++++++++++++++++ .../src/ReactFiberHooks.new.js | 4 +- .../src/ReactFiberHooks.old.js | 4 +- .../src/__tests__/useEvent-test.js | 2 +- packages/react-server/src/ReactFizzHooks.js | 14 +++ scripts/error-codes/codes.json | 2 +- 6 files changed, 123 insertions(+), 4 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 225d3657e0469..f89074a83124f 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -5545,4 +5545,105 @@ 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( + "A function wrapped in useEvent can't be called during rendering.", + ); + 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(() => {}); + if (onClick1 === onClick2) { + return
; + } else { + return ; + } + } + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual(
); + + const errors = []; + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + errors.push(error); + }, + }); + expect(() => { + expect(Scheduler).toFlushAndYield([]); + }).toErrorDev( + [ + 'Expected server HTML to contain a matching in
', + 'An error occurred during hydration', + ], + {withoutStack: 1}, + ); + expect(errors.length).toEqual(2); + expect(getVisibleChildren(container)).toEqual(); + }); + }); }); diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 2f3f9a4761b66..20fb29016eaf9 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -1877,7 +1877,9 @@ function mountEvent(callback: () => T): () => T { function event() { if (isInvalidExecutionContextForEventFunction()) { - throw new Error('An event from useEvent was called during render.'); + throw new Error( + "A function wrapped in useEvent can't be called during rendering.", + ); } return ref.current.apply(undefined, arguments); } diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index 1b41711186626..28584bf13114f 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -1877,7 +1877,9 @@ function mountEvent(callback: () => T): () => T { function event() { if (isInvalidExecutionContextForEventFunction()) { - throw new Error('An event from useEvent was called during render.'); + throw new Error( + "A function wrapped in useEvent can't be called during rendering.", + ); } return ref.current.apply(undefined, arguments); } diff --git a/packages/react-reconciler/src/__tests__/useEvent-test.js b/packages/react-reconciler/src/__tests__/useEvent-test.js index 39c0f9f316ef3..eaa6746acc605 100644 --- a/packages/react-reconciler/src/__tests__/useEvent-test.js +++ b/packages/react-reconciler/src/__tests__/useEvent-test.js @@ -195,7 +195,7 @@ describe('useEvent', () => { ReactNoop.render(); expect(Scheduler).toFlushAndThrow( - 'An event from useEvent was called during render', + "A function wrapped in useEvent can't be called during rendering.", ); // If something throws, we try one more time synchronously in case the error was diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js index f6c8847a81b2d..db406632a80e6 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( + "A function wrapped in useEvent can't be called during 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. @@ -675,6 +686,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..23889e317b192 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -425,6 +425,6 @@ "437": "the \"precedence\" prop for links to stylesheets expects to receive a string but received something of type \"%s\" instead.", "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.", + "440": "A function wrapped in useEvent can't be called during rendering.", "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." }