@samitouri / QOS-React-2 / commits / 3de9264496

[Fizz] experimental_useEvent (#25325)

* [Fizz] useEvent * Use same message on client and server

dan committed Sep 27, 2022 at 20:42 UTC 3de926449681015c4f1eec1147baa03a3cb823f5
6 files changed +123 -4
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+101
@@ -5545,4 +5545,105 @@ describe('ReactDOMFizzServer', () => {
5545 expect(getVisibleChildren(container)).toEqual('Hi');
5546 });
5547 });
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 + "A function wrapped in useEvent can't be called during rendering.",
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 + if (onClick1 === onClick2) {
5619 + return <div />;
5620 + } else {
5621 + return <span />;
5622 + }
5623 + }
5624 + await act(async () => {
5625 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
5626 + pipe(writable);
5627 + });
5628 + expect(getVisibleChildren(container)).toEqual(<div />);
5629 +
5630 + const errors = [];
5631 + ReactDOMClient.hydrateRoot(container, <App />, {
5632 + onRecoverableError(error) {
5633 + errors.push(error);
5634 + },
5635 + });
5636 + expect(() => {
5637 + expect(Scheduler).toFlushAndYield([]);
5638 + }).toErrorDev(
5639 + [
5640 + 'Expected server HTML to contain a matching <span> in <div>',
5641 + 'An error occurred during hydration',
5642 + ],
5643 + {withoutStack: 1},
5644 + );
5645 + expect(errors.length).toEqual(2);
5646 + expect(getVisibleChildren(container)).toEqual(<span />);
5647 + });
5648 + });
5649 });
packages/react-reconciler/src/ReactFiberHooks.new.js
+3 -1
@@ -1877,7 +1877,9 @@ function mountEvent<T>(callback: () => T): () => T {
1877
1878 function event() {
1879 if (isInvalidExecutionContextForEventFunction()) {
1880 - throw new Error('An event from useEvent was called during render.');
1880 + throw new Error(
1881 + "A function wrapped in useEvent can't be called during rendering.",
1882 + );
1883 }
1884 return ref.current.apply(undefined, arguments);
1885 }
packages/react-reconciler/src/ReactFiberHooks.old.js
+3 -1
@@ -1877,7 +1877,9 @@ function mountEvent<T>(callback: () => T): () => T {
1877
1878 function event() {
1879 if (isInvalidExecutionContextForEventFunction()) {
1880 - throw new Error('An event from useEvent was called during render.');
1880 + throw new Error(
1881 + "A function wrapped in useEvent can't be called during rendering.",
1882 + );
1883 }
1884 return ref.current.apply(undefined, arguments);
1885 }
packages/react-reconciler/src/__tests__/useEvent-test.js
+1 -1
@@ -195,7 +195,7 @@ describe('useEvent', () => {
195
196 ReactNoop.render(<Counter incrementBy={1} />);
197 expect(Scheduler).toFlushAndThrow(
198 - 'An event from useEvent was called during render',
198 + "A function wrapped in useEvent can't be called during rendering.",
199 );
200
201 // If something throws, we try one more time synchronously in case the error was
packages/react-server/src/ReactFizzHooks.js
+14
@@ -36,6 +36,7 @@ import {makeId} from './ReactServerFormatConfig';
36 import {
37 enableCache,
38 enableUseHook,
39 + enableUseEventHook,
40 enableUseMemoCacheHook,
41 } from 'shared/ReactFeatureFlags';
42 import is from 'shared/objectIs';
@@ -502,6 +503,16 @@ export function useCallback<T>(
503 return useMemo(() => callback, deps);
504 }
505
506 +function throwOnUseEventCall() {
507 + throw new Error(
508 + "A function wrapped in useEvent can't be called during rendering.",
509 + );
510 +}
511 +
512 +export function useEvent<T>(callback: () => T): () => T {
513 + return throwOnUseEventCall;
514 +}
515 +
516 // TODO Decide on how to implement this hook for server rendering.
517 // If a mutation occurs during render, consider triggering a Suspense boundary
518 // and falling back to client rendering.
@@ -675,6 +686,9 @@ if (enableCache) {
686 Dispatcher.getCacheForType = getCacheForType;
687 Dispatcher.useCacheRefresh = useCacheRefresh;
688 }
689 +if (enableUseEventHook) {
690 + Dispatcher.useEvent = useEvent;
691 +}
692 if (enableUseMemoCacheHook) {
693 Dispatcher.useMemoCache = useMemoCache;
694 }
scripts/error-codes/codes.json
+1 -1
@@ -425,6 +425,6 @@
425 "437": "the \"precedence\" prop for links to stylesheets expects to receive a string but received something of type \"%s\" instead.",
426 "438": "An unsupported type was passed to use(): %s",
427 "439": "We didn't expect to see a forward reference. This is a bug in the React Server.",
428 - "440": "An event from useEvent was called during render.",
428 + "440": "A function wrapped in useEvent can't be called during rendering.",
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 }