Skip to content

Commit f687157

Browse files
author
Jack Pope
committed
Add documentation for ref cleanup functions
1 parent 07cbd00 commit f687157

File tree

2 files changed

+65
-44
lines changed

2 files changed

+65
-44
lines changed

src/content/learn/manipulating-the-dom-with-refs.md

Lines changed: 57 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -211,25 +211,26 @@ This is because **Hooks must only be called at the top-level of your component.*
211211

212212
One possible way around this is to get a single ref to their parent element, and then use DOM manipulation methods like [`querySelectorAll`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll) to "find" the individual child nodes from it. However, this is brittle and can break if your DOM structure changes.
213213

214-
Another solution is to **pass a function to the `ref` attribute.** This is called a [`ref` callback.](/reference/react-dom/components/common#ref-callback) React will call your ref callback with the DOM node when it's time to set the ref, and with `null` when it's time to clear it. This lets you maintain your own array or a [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map), and access any ref by its index or some kind of ID.
214+
Another solution is to **pass a function to the `ref` attribute.** This is called a [`ref` callback.](/reference/react-dom/components/common#ref-callback) React will call your ref callback with the DOM node when it's time to set the ref, and call your cleanup function when it's time to clear it. This lets you maintain your own array or a [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map), and access any ref by its index or some kind of ID.
215215

216216
This example shows how you can use this approach to scroll to an arbitrary node in a long list:
217217

218218
<Sandpack>
219219

220220
```js
221-
import { useRef } from 'react';
221+
import { useRef, useState } from "react";
222222

223223
export default function CatFriends() {
224224
const itemsRef = useRef(null);
225+
const [catList, setCatList] = useState(setupCatList);
225226

226-
function scrollToId(itemId) {
227+
function scrollToCat(cat) {
227228
const map = getMap();
228-
const node = map.get(itemId);
229+
const node = map.get(cat);
229230
node.scrollIntoView({
230-
behavior: 'smooth',
231-
block: 'nearest',
232-
inline: 'center'
231+
behavior: "smooth",
232+
block: "nearest",
233+
inline: "center",
233234
});
234235
}
235236

@@ -241,37 +242,38 @@ export default function CatFriends() {
241242
return itemsRef.current;
242243
}
243244

245+
async function addCat() {
246+
setCatList((prev) => [createCatImg(prev.length + 100), ...prev]);
247+
}
248+
249+
async function removeCat() {
250+
setCatList((prev) => prev.slice(1));
251+
}
252+
244253
return (
245254
<>
246255
<nav>
247-
<button onClick={() => scrollToId(0)}>
248-
Tom
249-
</button>
250-
<button onClick={() => scrollToId(5)}>
251-
Maru
252-
</button>
253-
<button onClick={() => scrollToId(9)}>
254-
Jellylorum
255-
</button>
256+
<button onClick={addCat}>Add Cat</button>
257+
<button onClick={removeCat}>Remove Cat</button>
258+
<button onClick={() => scrollToCat(catList[0])}>Tom</button>
259+
<button onClick={() => scrollToCat(catList[5])}>Maru</button>
260+
<button onClick={() => scrollToCat(catList[9])}>Jellylorum</button>
256261
</nav>
257262
<div>
258263
<ul>
259-
{catList.map(cat => (
264+
{catList.map((cat) => (
260265
<li
261-
key={cat.id}
266+
key={cat}
262267
ref={(node) => {
263268
const map = getMap();
264-
if (node) {
265-
map.set(cat.id, node);
266-
} else {
267-
map.delete(cat.id);
268-
}
269+
map.set(cat, node);
270+
271+
return () => {
272+
map.delete(cat);
273+
};
269274
}}
270275
>
271-
<img
272-
src={cat.imageUrl}
273-
alt={'Cat #' + cat.id}
274-
/>
276+
<img src={cat} />
275277
</li>
276278
))}
277279
</ul>
@@ -280,12 +282,17 @@ export default function CatFriends() {
280282
);
281283
}
282284

283-
const catList = [];
284-
for (let i = 0; i < 10; i++) {
285-
catList.push({
286-
id: i,
287-
imageUrl: 'https://placekitten.com/250/200?image=' + i
288-
});
285+
function setupCatList() {
286+
const catList = [];
287+
for (let i = 0; i < 10; i++) {
288+
catList.push(createCatImg(i));
289+
}
290+
291+
return catList;
292+
}
293+
294+
function createCatImg(i) {
295+
return "https://loremflickr.com/320/240/cat?lock=" + i;
289296
}
290297

291298
```
@@ -316,6 +323,16 @@ li {
316323
}
317324
```
318325

326+
```json package.json hidden
327+
{
328+
"dependencies": {
329+
"react": "canary",
330+
"react-dom": "canary",
331+
"react-scripts": "^5.0.0"
332+
}
333+
}
334+
```
335+
319336
</Sandpack>
320337

321338
In this example, `itemsRef` doesn't hold a single DOM node. Instead, it holds a [Map](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map) from item ID to a DOM node. ([Refs can hold any values!](/learn/referencing-values-with-refs)) The [`ref` callback](/reference/react-dom/components/common#ref-callback) on every list item takes care to update the Map:
@@ -325,13 +342,13 @@ In this example, `itemsRef` doesn't hold a single DOM node. Instead, it holds a
325342
key={cat.id}
326343
ref={node => {
327344
const map = getMap();
328-
if (node) {
329-
// Add to the Map
330-
map.set(cat.id, node);
331-
} else {
332-
// Remove from the Map
333-
map.delete(cat.id);
334-
}
345+
// Add to the map
346+
map.set(cat, node);
347+
348+
return () => {
349+
// Remove from the map
350+
map.delete(cat);
351+
};
335352
}}
336353
>
337354
```

src/content/reference/react-dom/components/common.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -251,17 +251,21 @@ Instead of a ref object (like the one returned by [`useRef`](/reference/react/us
251251

252252
[See an example of using the `ref` callback.](/learn/manipulating-the-dom-with-refs#how-to-manage-a-list-of-refs-using-a-ref-callback)
253253

254-
When the `<div>` DOM node is added to the screen, React will call your `ref` callback with the DOM `node` as the argument. When that `<div>` DOM node is removed, React will call your `ref` callback with `null`.
254+
When the `<div>` DOM node is added to the screen, React will call your `ref` callback with the DOM `node` as the argument. If a cleanup function is returned, React will call it when that `<div>` DOM node is removed. Without a cleanup function, React will call your `ref` callback again with `null` when the node is removed.
255255

256-
React will also call your `ref` callback whenever you pass a *different* `ref` callback. In the above example, `(node) => { ... }` is a different function on every render. When your component re-renders, the *previous* function will be called with `null` as the argument, and the *next* function will be called with the DOM node.
256+
React will also call your `ref` callback whenever you pass a *different* `ref` callback. In the above example, `(node) => { ... }` is a different function on every render. When your component re-renders, React will call the *previous* callback's cleanup function if provided. If not cleanup function is defined, the `ref` callback will be called with `null` as the argument. The *next* function will be called with the DOM node.
257257

258258
#### Parameters {/*ref-callback-parameters*/}
259259

260-
* `node`: A DOM node or `null`. React will pass you the DOM node when the ref gets attached, and `null` when the ref gets detached. Unless you pass the same function reference for the `ref` callback on every render, the callback will get temporarily detached and re-attached during every re-render of the component.
260+
* `node`: A DOM node or `null`. React will pass you the DOM node when the ref gets attached, and `null` when the `ref` gets detached if no cleanup function is returned. Unless you pass the same function reference for the `ref` callback on every render, the callback will get temporarily detached and re-attached during every re-render of the component.
261261

262262
#### Returns {/*returns*/}
263263

264-
Do not return anything from the `ref` callback.
264+
* <CanaryBadge title="This feature is only available in the Canary channel" /> **optional** `cleanup function`: When the `ref` is detached, React will call the cleanup function. If a function is not returned by the `ref` callback, React will call the callback again with `null` as the argument when the `ref` gets detached.
265+
266+
#### Caveats {/*caveats*/}
267+
268+
* <CanaryBadge title="This feature is only available in the Canary channel" /> When Strict Mode is on, React will **run one extra development-only setup+cleanup cycle** before the first real setup. This is a stress-test that ensures that your cleanup logic "mirrors" your setup logic and that it stops or undoes whatever the setup is doing. If this causes a problem, implement the cleanup function.
265269

266270
---
267271

0 commit comments

Comments
 (0)