Skip to content

Commit 3de9264

Browse files
authored
[Fizz] experimental_useEvent (#25325)
* [Fizz] useEvent * Use same message on client and server
1 parent ae7ad8b commit 3de9264

File tree

6 files changed

+123
-4
lines changed

6 files changed

+123
-4
lines changed

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

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5545,4 +5545,105 @@ 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+
"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+
});
55485649
});

packages/react-reconciler/src/ReactFiberHooks.new.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1877,7 +1877,9 @@ function mountEvent<T>(callback: () => T): () => T {
18771877

18781878
function event() {
18791879
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+
);
18811883
}
18821884
return ref.current.apply(undefined, arguments);
18831885
}

packages/react-reconciler/src/ReactFiberHooks.old.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1877,7 +1877,9 @@ function mountEvent<T>(callback: () => T): () => T {
18771877

18781878
function event() {
18791879
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+
);
18811883
}
18821884
return ref.current.apply(undefined, arguments);
18831885
}

packages/react-reconciler/src/__tests__/useEvent-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ describe('useEvent', () => {
195195

196196
ReactNoop.render(<Counter incrementBy={1} />);
197197
expect(Scheduler).toFlushAndThrow(
198-
'An event from useEvent was called during render',
198+
"A function wrapped in useEvent can't be called during rendering.",
199199
);
200200

201201
// If something throws, we try one more time synchronously in case the error was

packages/react-server/src/ReactFizzHooks.js

Lines changed: 14 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+
"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+
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.
@@ -675,6 +686,9 @@ if (enableCache) {
675686
Dispatcher.getCacheForType = getCacheForType;
676687
Dispatcher.useCacheRefresh = useCacheRefresh;
677688
}
689+
if (enableUseEventHook) {
690+
Dispatcher.useEvent = useEvent;
691+
}
678692
if (enableUseMemoCacheHook) {
679693
Dispatcher.useMemoCache = useMemoCache;
680694
}

scripts/error-codes/codes.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,6 @@
425425
"437": "the \"precedence\" prop for links to stylesheets expects to receive a string but received something of type \"%s\" instead.",
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.",
428-
"440": "An event from useEvent was called during render.",
428+
"440": "A function wrapped in useEvent can't be called during rendering.",
429429
"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."
430430
}

0 commit comments

Comments
 (0)