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

feat: Add useHydrateableEffect #17350

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js
Expand Up @@ -1325,6 +1325,7 @@ function getReactiveHookCallbackIndex(calleeNode, options) {
switch (node.name) {
case 'useEffect':
case 'useLayoutEffect':
case 'useHydrateableEffect':
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not so sure about the naming. We don't really hydrate an effect since it those are never executed on the server anyway. I only used hydration in the name to communicate that the behavior of the hook is tied to hydration.

case 'useCallback':
case 'useMemo':
// useEffect(fn)
Expand Down
14 changes: 14 additions & 0 deletions packages/react-debug-tools/src/ReactDebugHooks.js
Expand Up @@ -62,6 +62,7 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
Dispatcher.useReducer((s, a) => s, null);
Dispatcher.useRef(null);
Dispatcher.useLayoutEffect(() => {});
Dispatcher.useHydrateableEffect(() => {});
Dispatcher.useEffect(() => {});
Dispatcher.useImperativeHandle(undefined, () => null);
Dispatcher.useDebugValue(null);
Expand Down Expand Up @@ -167,6 +168,18 @@ function useLayoutEffect(
});
}

function useHydrateableEffect(
create: () => (() => void) | void,
inputs: Array<mixed> | void | null,
): void {
nextHook();
hookLog.push({
primitive: 'HydrateableEffect',
stackError: new Error(),
value: create,
});
}

function useEffect(
create: () => (() => void) | void,
inputs: Array<mixed> | void | null,
Expand Down Expand Up @@ -270,6 +283,7 @@ const Dispatcher: DispatcherType = {
useImperativeHandle,
useDebugValue,
useLayoutEffect,
useHydrateableEffect,
useMemo,
useReducer,
useRef,
Expand Down
48 changes: 48 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMRoot-test.js
Expand Up @@ -12,6 +12,7 @@
let React = require('react');
let ReactDOM = require('react-dom');
let ReactDOMServer = require('react-dom/server');
let ReactDOMTestUtils = require('react-dom/test-utils');
let Scheduler = require('scheduler');

describe('ReactDOMRoot', () => {
Expand All @@ -23,6 +24,7 @@ describe('ReactDOMRoot', () => {
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
ReactDOMTestUtils = require('react-dom/test-utils');
Scheduler = require('scheduler');
});

Expand Down Expand Up @@ -229,4 +231,50 @@ describe('ReactDOMRoot', () => {
Scheduler.unstable_flushAll();
ReactDOM.createRoot(container); // No warning
});

describe('useHydratedEffect', () => {
function Component({value}) {
const [state, setState] = React.useState('initial');

React.useHydrateableEffect(() => {
setState(`${value}-effect`);
});

return <p>{state}</p>;
}

it('behaves like useLayoutEffect when not hydrating', () => {
const root = ReactDOM.createBlockingRoot(container);

root.render(<Component />);
Scheduler.unstable_advanceTime(0);

expect(container.textContent).toEqual('undefined-effect');

root.render(<Component value="rerender" />);
Scheduler.unstable_advanceTime(0);

expect(container.textContent).toEqual('rerender-effect');
});

it('behaves like useEffect when hydrating', () => {
const root = ReactDOM.createBlockingRoot(container, {hydrate: true});
const button = document.createElement('p');
button.appendChild(document.createTextNode('initial'));
container.appendChild(button);

ReactDOMTestUtils.act(() => {
root.render(<Component />);
Scheduler.unstable_advanceTime(0);

expect(container.textContent).toEqual('initial');
});
expect(container.textContent).toEqual('undefined-effect');
});

it('behaves like useEffect when server-side rendering', () => {
const markup = ReactDOMServer.renderToString(<Component />);
expect(markup).toEqual('<p data-reactroot="">initial</p>');
});
});
});
1 change: 1 addition & 0 deletions packages/react-dom/src/server/ReactPartialRendererHooks.js
Expand Up @@ -495,6 +495,7 @@ export const Dispatcher: DispatcherType = {
useImperativeHandle: noop,
// Effects are not run in the server environment.
useEffect: noop,
useHydrateableEffect: noop,
// Debugging effect
useDebugValue: noop,
useResponder,
Expand Down
81 changes: 81 additions & 0 deletions packages/react-reconciler/src/ReactFiberHooks.js
Expand Up @@ -56,6 +56,7 @@ import {
runWithPriority,
getCurrentPriorityLevel,
} from './SchedulerWithReactIntegration';
import {getIsHydrating} from './ReactFiberHydrationContext';

const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;

Expand Down Expand Up @@ -83,6 +84,10 @@ export type Dispatcher = {|
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void,
useHydrateableEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void,
useCallback<T>(callback: T, deps: Array<mixed> | void | null): T,
useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T,
useImperativeHandle<T>(
Expand Down Expand Up @@ -124,6 +129,7 @@ export type HookType =
| 'useContext'
| 'useRef'
| 'useEffect'
| 'useHydrateableEffect'
| 'useLayoutEffect'
| 'useCallback'
| 'useMemo'
Expand Down Expand Up @@ -984,6 +990,17 @@ function updateEffect(
);
}

function mountHydrateableEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
if (getIsHydrating()) {
return mountEffect(create, deps);
} else {
return mountLayoutEffect(create, deps);
}
}

function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
Expand Down Expand Up @@ -1379,6 +1396,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
useEffect: throwInvalidHookError,
useImperativeHandle: throwInvalidHookError,
useLayoutEffect: throwInvalidHookError,
useHydrateableEffect: throwInvalidHookError,
useMemo: throwInvalidHookError,
useReducer: throwInvalidHookError,
useRef: throwInvalidHookError,
Expand All @@ -1397,6 +1415,7 @@ const HooksDispatcherOnMount: Dispatcher = {
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useHydrateableEffect: mountHydrateableEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
Expand All @@ -1415,6 +1434,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useHydrateableEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
Expand All @@ -1433,6 +1453,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useHydrateableEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: rerenderReducer,
useRef: updateRef,
Expand Down Expand Up @@ -1520,6 +1541,15 @@ if (__DEV__) {
checkDepsAreArrayDev(deps);
return mountLayoutEffect(create, deps);
},
useHydrateableEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useHydrateableEffect';
mountHookTypesDev();
checkDepsAreArrayDev(deps);
return mountHydrateableEffect(create, deps);
},
useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
currentHookNameInDev = 'useMemo';
mountHookTypesDev();
Expand Down Expand Up @@ -1638,6 +1668,14 @@ if (__DEV__) {
updateHookTypesDev();
return mountLayoutEffect(create, deps);
},
useHydrateableEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useHydrateableEffect';
updateHookTypesDev();
return mountHydrateableEffect(create, deps);
},
useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
currentHookNameInDev = 'useMemo';
updateHookTypesDev();
Expand Down Expand Up @@ -1755,6 +1793,14 @@ if (__DEV__) {
updateHookTypesDev();
return updateLayoutEffect(create, deps);
},
useHydrateableEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useHydrateableEffect';
updateHookTypesDev();
return updateLayoutEffect(create, deps);
},
useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
currentHookNameInDev = 'useMemo';
updateHookTypesDev();
Expand Down Expand Up @@ -1872,6 +1918,14 @@ if (__DEV__) {
updateHookTypesDev();
return updateLayoutEffect(create, deps);
},
useHydrateableEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useHydrateableEffect';
updateHookTypesDev();
return updateLayoutEffect(create, deps);
},
useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
currentHookNameInDev = 'useMemo';
updateHookTypesDev();
Expand Down Expand Up @@ -1995,6 +2049,15 @@ if (__DEV__) {
mountHookTypesDev();
return mountLayoutEffect(create, deps);
},
useHydrateableEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useHydrateableEffect';
warnInvalidHookAccess();
mountHookTypesDev();
return mountHydrateableEffect(create, deps);
},
useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
currentHookNameInDev = 'useMemo';
warnInvalidHookAccess();
Expand Down Expand Up @@ -2126,6 +2189,15 @@ if (__DEV__) {
updateHookTypesDev();
return updateLayoutEffect(create, deps);
},
useHydrateableEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useHydrateableEffect';
warnInvalidHookAccess();
updateHookTypesDev();
return updateLayoutEffect(create, deps);
},
useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
currentHookNameInDev = 'useMemo';
warnInvalidHookAccess();
Expand Down Expand Up @@ -2257,6 +2329,15 @@ if (__DEV__) {
updateHookTypesDev();
return updateLayoutEffect(create, deps);
},
useHydrateableEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useHydrateableEffect';
warnInvalidHookAccess();
updateHookTypesDev();
return updateLayoutEffect(create, deps);
},
useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
currentHookNameInDev = 'useMemo';
warnInvalidHookAccess();
Expand Down
2 changes: 2 additions & 0 deletions packages/react-refresh/src/ReactFreshBabelPlugin.js
Expand Up @@ -220,6 +220,8 @@ export default function(babel, opts = {}) {
case 'React.useEffect':
case 'useLayoutEffect':
case 'React.useLayoutEffect':
case 'useHydrateableEffect':
case 'React.useHydrateableEffect':
case 'useMemo':
case 'React.useMemo':
case 'useCallback':
Expand Down
1 change: 1 addition & 0 deletions packages/react-test-renderer/src/ReactShallowRenderer.js
Expand Up @@ -408,6 +408,7 @@ class ReactShallowRenderer {
useEffect: noOp,
useImperativeHandle: noOp,
useLayoutEffect: noOp,
useHydrateableEffect: noOp,
useMemo,
useReducer,
useRef,
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/React.js
Expand Up @@ -36,6 +36,7 @@ import {
useImperativeHandle,
useDebugValue,
useLayoutEffect,
useHydrateableEffect,
useMemo,
useReducer,
useRef,
Expand Down Expand Up @@ -90,6 +91,7 @@ const React = {
useImperativeHandle,
useDebugValue,
useLayoutEffect,
useHydrateableEffect,
useMemo,
useReducer,
useRef,
Expand Down
8 changes: 8 additions & 0 deletions packages/react/src/ReactHooks.js
Expand Up @@ -112,6 +112,14 @@ export function useLayoutEffect(
return dispatcher.useLayoutEffect(create, deps);
}

export function useHydrateableEffect(
create: () => (() => void) | void,
inputs: Array<mixed> | void | null,
) {
const dispatcher = resolveDispatcher();
return dispatcher.useHydrateableEffect(create, inputs);
}

export function useCallback<T>(
callback: T,
deps: Array<mixed> | void | null,
Expand Down