Skip to content

Commit

Permalink
[Fizz] experimental_useEvent (#25325)
Browse files Browse the repository at this point in the history
* [Fizz] useEvent

* Use same message on client and server
  • Loading branch information
gaearon committed Sep 27, 2022
1 parent ae7ad8b commit 3de9264
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 4 deletions.
101 changes: 101 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<button ref={ref} onClick={() => onClick()}>
{count}
</button>
);
}
await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});
expect(getVisibleChildren(container)).toEqual(<button>0</button>);

ReactDOMClient.hydrateRoot(container, <App />);
expect(Scheduler).toFlushAndYield([]);
expect(getVisibleChildren(container)).toEqual(<button>0</button>);

ref.current.dispatchEvent(
new window.MouseEvent('click', {bubbles: true}),
);
await jest.runAllTimers();
expect(getVisibleChildren(container)).toEqual(<button>1</button>);
});

// @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 <p>Hello</p>;
}

const reportedServerErrors = [];
let caughtError;
try {
await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />, {
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 <div />;
} else {
return <span />;
}
}
await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});
expect(getVisibleChildren(container)).toEqual(<div />);

const errors = [];
ReactDOMClient.hydrateRoot(container, <App />, {
onRecoverableError(error) {
errors.push(error);
},
});
expect(() => {
expect(Scheduler).toFlushAndYield([]);
}).toErrorDev(
[
'Expected server HTML to contain a matching <span> in <div>',
'An error occurred during hydration',
],
{withoutStack: 1},
);
expect(errors.length).toEqual(2);
expect(getVisibleChildren(container)).toEqual(<span />);
});
});
});
4 changes: 3 additions & 1 deletion packages/react-reconciler/src/ReactFiberHooks.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -1877,7 +1877,9 @@ function mountEvent<T>(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);
}
Expand Down
4 changes: 3 additions & 1 deletion packages/react-reconciler/src/ReactFiberHooks.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -1877,7 +1877,9 @@ function mountEvent<T>(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);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/react-reconciler/src/__tests__/useEvent-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ describe('useEvent', () => {

ReactNoop.render(<Counter incrementBy={1} />);
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
Expand Down
14 changes: 14 additions & 0 deletions packages/react-server/src/ReactFizzHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {makeId} from './ReactServerFormatConfig';
import {
enableCache,
enableUseHook,
enableUseEventHook,
enableUseMemoCacheHook,
} from 'shared/ReactFeatureFlags';
import is from 'shared/objectIs';
Expand Down Expand Up @@ -502,6 +503,16 @@ export function useCallback<T>(
return useMemo(() => callback, deps);
}

function throwOnUseEventCall() {
throw new Error(
"A function wrapped in useEvent can't be called during rendering.",
);
}

export function useEvent<T>(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.
Expand Down Expand Up @@ -675,6 +686,9 @@ if (enableCache) {
Dispatcher.getCacheForType = getCacheForType;
Dispatcher.useCacheRefresh = useCacheRefresh;
}
if (enableUseEventHook) {
Dispatcher.useEvent = useEvent;
}
if (enableUseMemoCacheHook) {
Dispatcher.useMemoCache = useMemoCache;
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/error-codes/codes.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}

0 comments on commit 3de9264

Please sign in to comment.