Add ref cleanup function (#25686)

Add option for ref function to return a clean up function.

```jsx
<div ref={(_ref) => {
  // Use `_ref`
  return () => {
    // Clean up _ref
  };
}} />
```

If clean up function is not provided. Ref function is called with null
like it has been before.

```jsx
<div ref={(_ref) => {
  if (_ref) {
    // Use _ref
  } else {
    // Clean up _ref
  }
}} />
```
This commit is contained in:
Samuel Susla
2022-11-29 16:41:02 +00:00
committed by GitHub
parent edbfc6399f
commit e98225485a
6 changed files with 195 additions and 99 deletions
+133 -29
View File
@@ -401,35 +401,6 @@ describe('ref swapping', () => {
'Expected ref to be a function, a string, an object returned by React.createRef(), or null.',
);
});
it('should warn about callback refs returning a function', () => {
const container = document.createElement('div');
expect(() => {
ReactDOM.render(<div ref={() => () => {}} />, container);
}).toErrorDev('Unexpected return value from a callback ref in div');
// Cleanup should warn, too.
expect(() => {
ReactDOM.render(<span />, container);
}).toErrorDev('Unexpected return value from a callback ref in div', {
withoutStack: true,
});
// No warning when returning non-functions.
ReactDOM.render(<p ref={() => ({})} />, container);
ReactDOM.render(<p ref={() => null} />, container);
ReactDOM.render(<p ref={() => undefined} />, container);
// Still warns on functions (not deduped).
expect(() => {
ReactDOM.render(<div ref={() => () => {}} />, container);
}).toErrorDev('Unexpected return value from a callback ref in div');
expect(() => {
ReactDOM.unmountComponentAtNode(container);
}).toErrorDev('Unexpected return value from a callback ref in div', {
withoutStack: true,
});
});
});
describe('root level refs', () => {
@@ -612,3 +583,136 @@ describe('strings refs across renderers', () => {
expect(inst.refs.child2).toBe(div2.firstChild);
});
});
describe('refs return clean up function', () => {
it('calls clean up function if it exists', () => {
const container = document.createElement('div');
let cleanUp = jest.fn();
let setup = jest.fn();
ReactDOM.render(
<div
ref={_ref => {
setup(_ref);
return cleanUp;
}}
/>,
container,
);
ReactDOM.render(
<div
ref={_ref => {
setup(_ref);
}}
/>,
container,
);
expect(setup).toHaveBeenCalledTimes(2);
expect(cleanUp).toHaveBeenCalledTimes(1);
expect(cleanUp.mock.calls[0][0]).toBe(undefined);
ReactDOM.render(<div ref={_ref => {}} />, container);
expect(cleanUp).toHaveBeenCalledTimes(1);
expect(setup).toHaveBeenCalledTimes(3);
expect(setup.mock.calls[2][0]).toBe(null);
cleanUp = jest.fn();
setup = jest.fn();
ReactDOM.render(
<div
ref={_ref => {
setup(_ref);
return cleanUp;
}}
/>,
container,
);
expect(setup).toHaveBeenCalledTimes(1);
expect(cleanUp).toHaveBeenCalledTimes(0);
ReactDOM.render(
<div
ref={_ref => {
setup(_ref);
return cleanUp;
}}
/>,
container,
);
expect(setup).toHaveBeenCalledTimes(2);
expect(cleanUp).toHaveBeenCalledTimes(1);
});
it('handles ref functions with stable identity', () => {
const container = document.createElement('div');
const cleanUp = jest.fn();
const setup = jest.fn();
function _onRefChange(_ref) {
setup(_ref);
return cleanUp;
}
ReactDOM.render(<div ref={_onRefChange} />, container);
expect(setup).toHaveBeenCalledTimes(1);
expect(cleanUp).toHaveBeenCalledTimes(0);
ReactDOM.render(
<div className="niceClassName" ref={_onRefChange} />,
container,
);
expect(setup).toHaveBeenCalledTimes(1);
expect(cleanUp).toHaveBeenCalledTimes(0);
ReactDOM.render(<div />, container);
expect(setup).toHaveBeenCalledTimes(1);
expect(cleanUp).toHaveBeenCalledTimes(1);
});
it('warns if clean up function is returned when called with null', () => {
const container = document.createElement('div');
const cleanUp = jest.fn();
const setup = jest.fn();
let returnCleanUp = false;
ReactDOM.render(
<div
ref={_ref => {
setup(_ref);
if (returnCleanUp) {
return cleanUp;
}
}}
/>,
container,
);
expect(setup).toHaveBeenCalledTimes(1);
expect(cleanUp).toHaveBeenCalledTimes(0);
returnCleanUp = true;
expect(() => {
ReactDOM.render(
<div
ref={_ref => {
setup(_ref);
if (returnCleanUp) {
return cleanUp;
}
}}
/>,
container,
);
}).toErrorDev('Unexpected return value from a callback ref in div');
});
});
@@ -148,6 +148,7 @@ function FiberNode(
this.index = 0;
this.ref = null;
this.refCleanup = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
@@ -336,6 +337,7 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;
workInProgress.refCleanup = current.refCleanup;
if (enableProfilerTimer) {
workInProgress.selfBaseDuration = current.selfBaseDuration;
@@ -880,6 +882,7 @@ export function assignFiberPropertiesInDEV(
target.sibling = source.sibling;
target.index = source.index;
target.ref = source.ref;
target.refCleanup = source.refCleanup;
target.pendingProps = source.pendingProps;
target.memoizedProps = source.memoizedProps;
target.updateQueue = source.updateQueue;
@@ -148,6 +148,7 @@ function FiberNode(
this.index = 0;
this.ref = null;
this.refCleanup = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
@@ -336,6 +337,7 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;
workInProgress.refCleanup = current.refCleanup;
if (enableProfilerTimer) {
workInProgress.selfBaseDuration = current.selfBaseDuration;
@@ -880,6 +882,7 @@ export function assignFiberPropertiesInDEV(
target.sibling = source.sibling;
target.index = source.index;
target.ref = source.ref;
target.refCleanup = source.refCleanup;
target.pendingProps = source.pendingProps;
target.memoizedProps = source.memoizedProps;
target.updateQueue = source.updateQueue;
@@ -286,8 +286,32 @@ function safelyAttachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
const ref = current.ref;
const refCleanup = current.refCleanup;
if (ref !== null) {
if (typeof ref === 'function') {
if (typeof refCleanup === 'function') {
try {
if (shouldProfile(current)) {
try {
startLayoutEffectTimer();
refCleanup();
} finally {
recordLayoutEffectDuration(current);
}
} else {
refCleanup();
}
} catch (error) {
captureCommitPhaseError(current, nearestMountedAncestor, error);
} finally {
// `refCleanup` has been called. Nullify all references to it to prevent double invocation.
current.refCleanup = null;
const finishedWork = current.alternate;
if (finishedWork != null) {
finishedWork.refCleanup = null;
}
}
} else if (typeof ref === 'function') {
let retVal;
try {
if (shouldProfile(current)) {
@@ -1572,25 +1596,15 @@ function commitAttachRef(finishedWork: Fiber) {
instanceToUse = instance;
}
if (typeof ref === 'function') {
let retVal;
if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
retVal = ref(instanceToUse);
finishedWork.refCleanup = ref(instanceToUse);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
retVal = ref(instanceToUse);
}
if (__DEV__) {
if (typeof retVal === 'function') {
console.error(
'Unexpected return value from a callback ref in %s. ' +
'A callback ref should not return a function.',
getComponentNameFromFiber(finishedWork),
);
}
finishedWork.refCleanup = ref(instanceToUse);
}
} else {
if (__DEV__) {
@@ -1609,27 +1623,6 @@ function commitAttachRef(finishedWork: Fiber) {
}
}
function commitDetachRef(current: Fiber) {
const currentRef = current.ref;
if (currentRef !== null) {
if (typeof currentRef === 'function') {
if (shouldProfile(current)) {
try {
startLayoutEffectTimer();
currentRef(null);
} finally {
recordLayoutEffectDuration(current);
}
} else {
currentRef(null);
}
} else {
// $FlowFixMe unable to narrow type to the non-function case
currentRef.current = null;
}
}
}
function detachFiberMutation(fiber: Fiber) {
// Cut off the return pointer to disconnect it from the tree.
// This enables us to detect and warn against state updates on an unmounted component.
@@ -4450,7 +4443,6 @@ function invokePassiveEffectUnmountInDEV(fiber: Fiber): void {
export {
commitPlacement,
commitAttachRef,
commitDetachRef,
invokeLayoutEffectMountInDEV,
invokeLayoutEffectUnmountInDEV,
invokePassiveEffectMountInDEV,
@@ -286,8 +286,32 @@ function safelyAttachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
const ref = current.ref;
const refCleanup = current.refCleanup;
if (ref !== null) {
if (typeof ref === 'function') {
if (typeof refCleanup === 'function') {
try {
if (shouldProfile(current)) {
try {
startLayoutEffectTimer();
refCleanup();
} finally {
recordLayoutEffectDuration(current);
}
} else {
refCleanup();
}
} catch (error) {
captureCommitPhaseError(current, nearestMountedAncestor, error);
} finally {
// `refCleanup` has been called. Nullify all references to it to prevent double invocation.
current.refCleanup = null;
const finishedWork = current.alternate;
if (finishedWork != null) {
finishedWork.refCleanup = null;
}
}
} else if (typeof ref === 'function') {
let retVal;
try {
if (shouldProfile(current)) {
@@ -1572,25 +1596,15 @@ function commitAttachRef(finishedWork: Fiber) {
instanceToUse = instance;
}
if (typeof ref === 'function') {
let retVal;
if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
retVal = ref(instanceToUse);
finishedWork.refCleanup = ref(instanceToUse);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
retVal = ref(instanceToUse);
}
if (__DEV__) {
if (typeof retVal === 'function') {
console.error(
'Unexpected return value from a callback ref in %s. ' +
'A callback ref should not return a function.',
getComponentNameFromFiber(finishedWork),
);
}
finishedWork.refCleanup = ref(instanceToUse);
}
} else {
if (__DEV__) {
@@ -1609,27 +1623,6 @@ function commitAttachRef(finishedWork: Fiber) {
}
}
function commitDetachRef(current: Fiber) {
const currentRef = current.ref;
if (currentRef !== null) {
if (typeof currentRef === 'function') {
if (shouldProfile(current)) {
try {
startLayoutEffectTimer();
currentRef(null);
} finally {
recordLayoutEffectDuration(current);
}
} else {
currentRef(null);
}
} else {
// $FlowFixMe unable to narrow type to the non-function case
currentRef.current = null;
}
}
}
function detachFiberMutation(fiber: Fiber) {
// Cut off the return pointer to disconnect it from the tree.
// This enables us to detect and warn against state updates on an unmounted component.
@@ -4450,7 +4443,6 @@ function invokePassiveEffectUnmountInDEV(fiber: Fiber): void {
export {
commitPlacement,
commitAttachRef,
commitDetachRef,
invokeLayoutEffectMountInDEV,
invokeLayoutEffectUnmountInDEV,
invokePassiveEffectMountInDEV,
+2
View File
@@ -131,6 +131,8 @@ export type Fiber = {
| (((handle: mixed) => void) & {_stringRef: ?string, ...})
| RefObject,
refCleanup: null | (() => void),
// Input is the data coming into process this fiber. Arguments. Props.
pendingProps: any, // This type will be more specific once we overload the tag.
memoizedProps: any, // The props used to create the output.