Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fizz] experimental_useEvent #25325

Merged
merged 2 commits into from
Sep 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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."
}