Skip to content

Commit

Permalink
Support ref cleanup function for imperative handle refs
Browse files Browse the repository at this point in the history
  • Loading branch information
kassens committed Apr 25, 2024
1 parent cf5ab8b commit 964ca88
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 17 deletions.
111 changes: 96 additions & 15 deletions packages/react-dom/src/__tests__/refs-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,16 @@

'use strict';

let React = require('react');
let ReactDOMClient = require('react-dom/client');
let act = require('internal-test-utils').act;
const React = require('react');
const ReactDOMClient = require('react-dom/client');
const act = require('internal-test-utils').act;

// This is testing if string refs are deleted from `instance.refs`
// Once support for string refs is removed, this test can be removed.
// Detaching is already tested in refs-detruction-test.js
describe('reactiverefs', () => {
let container;

beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOMClient = require('react-dom/client');
act = require('internal-test-utils').act;
});

afterEach(() => {
if (container) {
document.body.removeChild(container);
Expand Down Expand Up @@ -199,11 +192,6 @@ describe('reactiverefs', () => {
describe('ref swapping', () => {
let RefHopsAround;
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOMClient = require('react-dom/client');
act = require('internal-test-utils').act;

RefHopsAround = class extends React.Component {
container = null;
state = {count: 0};
Expand Down Expand Up @@ -804,3 +792,96 @@ describe('refs return clean up function', () => {
expect(nullHandler).toHaveBeenCalledTimes(0);
});
});

describe('useImerativeHandle refs', () => {
const ImperativeHandleComponent = React.forwardRef(({name}, ref) => {
React.useImperativeHandle(
ref,
() => ({
greet() {
return `Hello ${name}`;
},
}),
[name],
);
return null;
});

it('should work with object style refs', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
const ref = React.createRef();

await act(async () => {
root.render(<ImperativeHandleComponent name="Alice" ref={ref} />);
});
expect(ref.current.greet()).toBe('Hello Alice');
await act(() => {
root.render(null);
});
expect(ref.current).toBe(null);
});

it('should work with callback style refs', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
let current = null;

await act(async () => {
root.render(
<ImperativeHandleComponent
name="Alice"
ref={r => {
current = r;
}}
/>,
);
});
expect(current.greet()).toBe('Hello Alice');
await act(() => {
root.render(null);
});
expect(current).toBe(null);
});

it('should work with callback style refs with cleanup function', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);

let cleanupCalls = 0;
let createCalls = 0;
let current = null;

const ref = r => {
current = r;
createCalls++;
return () => {
current = null;
cleanupCalls++;
};
};

await act(async () => {
root.render(<ImperativeHandleComponent name="Alice" ref={ref} />);
});
expect(current.greet()).toBe('Hello Alice');
expect(createCalls).toBe(1);
expect(cleanupCalls).toBe(0);

// update a dep should recreate the ref
await act(async () => {
root.render(<ImperativeHandleComponent name="Bob" ref={ref} />);
});
expect(current.greet()).toBe('Hello Bob');
expect(createCalls).toBe(2);
expect(cleanupCalls).toBe(1);

// unmounting should call cleanup
await act(() => {
root.render(null);
});
expect(current).toBe(null);
expect(createCalls).toBe(2);
expect(cleanupCalls).toBe(2);
});
});
9 changes: 7 additions & 2 deletions packages/react-reconciler/src/ReactFiberHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -2564,9 +2564,14 @@ function imperativeHandleEffect<T>(
if (typeof ref === 'function') {
const refCallback = ref;
const inst = create();
refCallback(inst);
const refCleanup = refCallback(inst);
return () => {
refCallback(null);
if (typeof refCleanup === 'function') {
// $FlowFixMe[incompatible-use] we need to assume no parameters
refCleanup();
} else {
refCallback(null);
}
};
} else if (ref !== null && ref !== undefined) {
const refObject = ref;
Expand Down

0 comments on commit 964ca88

Please sign in to comment.