[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
}