From bad34fc6efd6ff733be7cd0f615eee4d27e72734 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Tue, 17 Dec 2024 02:11:51 -0800 Subject: [PATCH 1/7] Add hanging ref troubleshooting section --- .../reference/react-dom/components/common.md | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/content/reference/react-dom/components/common.md b/src/content/reference/react-dom/components/common.md index 9d15332139d..9c58a13d694 100644 --- a/src/content/reference/react-dom/components/common.md +++ b/src/content/reference/react-dom/components/common.md @@ -282,6 +282,90 @@ To support backwards compatibility, if a cleanup function is not returned from t * 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. * When you pass a *different* `ref` callback, React will call the *previous* callback's cleanup function if provided. If no 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. +#### Troubleshooting: My ref points to a unmounted DOM node {/*my-ref-points-to-a-unmounted-dom-node*/} + +A `ref` callback function with a cleanup function that does not set `ref.current` to `null` can result in a `ref` to a unmounted node. Uncheck "Show Input" below and click "Submit" to see how the `ref` to the unmounted `` is still accessible by the click handler for the form. + + + +```js +import { useRef, useState } from "react"; + +export default function MyForm() { + const [showInput, setShowInput] = useState(true); + let inputRef = useRef(); + const handleCheckboxChange = (event) => { + setShowInput(event.target.checked); + }; + const handleSubmit = (event) => { + event.preventDefault(); + if (inputRef.current) { + alert(`Input value is: "${inputRef.current.value}"`); + } else { + alert("no input"); + } + }; + const inputRefCallback = (node) => { + inputRef.current = node; + return () => { + // ⚠️ You must set `ref.current` to `null` + // in this cleanup function e.g. + // `inputRef.current = null;` + // to prevent hanging refs to unmounted DOM nodes + }; + }; + + return ( +
+
+ +
+ {showInput && ( +
+ +
+ )} + +
+ ); +} +``` + +
+ +To fix the hanging ref to the DOM node that is no longer rendered set the `ref.current` to `null` in the ref callback cleanup function. + +```js +import { useRef } from "react"; + +function MyInput() { + inputRef = useRef() + const inputRefCallback = (node) => { + ref.current = node; + return () => { + // ⚠️ You must set `ref.current` to `null` in this cleanup + // function to prevent hanging refs to unmounted DOM nodes + inputRef.current = null; + }; + }; + return +} +``` + --- ### React event object {/*react-event-object*/} From dc3b62ca27f7a3caa0e34ee5183ac380cbdbbbe4 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Tue, 17 Dec 2024 02:21:39 -0800 Subject: [PATCH 2/7] Reworded for clarity --- src/content/reference/react-dom/components/common.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/reference/react-dom/components/common.md b/src/content/reference/react-dom/components/common.md index 9c58a13d694..46fd81ba715 100644 --- a/src/content/reference/react-dom/components/common.md +++ b/src/content/reference/react-dom/components/common.md @@ -347,7 +347,7 @@ export default function MyForm() { -To fix the hanging ref to the DOM node that is no longer rendered set the `ref.current` to `null` in the ref callback cleanup function. +To fix the hanging ref to the DOM node that is no longer rendered, set `ref.current` to `null` in the `ref` callback cleanup function. ```js import { useRef } from "react"; From 17a31786e104b7317d3d32394655b1883526bba1 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Sat, 21 Dec 2024 00:23:32 -0800 Subject: [PATCH 3/7] Move content to useRef reference page --- .../reference/react-dom/components/common.md | 84 ----- src/content/reference/react/useRef.md | 304 ++++++++++++++++++ 2 files changed, 304 insertions(+), 84 deletions(-) diff --git a/src/content/reference/react-dom/components/common.md b/src/content/reference/react-dom/components/common.md index 46fd81ba715..9d15332139d 100644 --- a/src/content/reference/react-dom/components/common.md +++ b/src/content/reference/react-dom/components/common.md @@ -282,90 +282,6 @@ To support backwards compatibility, if a cleanup function is not returned from t * 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. * When you pass a *different* `ref` callback, React will call the *previous* callback's cleanup function if provided. If no 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. -#### Troubleshooting: My ref points to a unmounted DOM node {/*my-ref-points-to-a-unmounted-dom-node*/} - -A `ref` callback function with a cleanup function that does not set `ref.current` to `null` can result in a `ref` to a unmounted node. Uncheck "Show Input" below and click "Submit" to see how the `ref` to the unmounted `` is still accessible by the click handler for the form. - - - -```js -import { useRef, useState } from "react"; - -export default function MyForm() { - const [showInput, setShowInput] = useState(true); - let inputRef = useRef(); - const handleCheckboxChange = (event) => { - setShowInput(event.target.checked); - }; - const handleSubmit = (event) => { - event.preventDefault(); - if (inputRef.current) { - alert(`Input value is: "${inputRef.current.value}"`); - } else { - alert("no input"); - } - }; - const inputRefCallback = (node) => { - inputRef.current = node; - return () => { - // ⚠️ You must set `ref.current` to `null` - // in this cleanup function e.g. - // `inputRef.current = null;` - // to prevent hanging refs to unmounted DOM nodes - }; - }; - - return ( -
-
- -
- {showInput && ( -
- -
- )} - -
- ); -} -``` - -
- -To fix the hanging ref to the DOM node that is no longer rendered, set `ref.current` to `null` in the `ref` callback cleanup function. - -```js -import { useRef } from "react"; - -function MyInput() { - inputRef = useRef() - const inputRefCallback = (node) => { - ref.current = node; - return () => { - // ⚠️ You must set `ref.current` to `null` in this cleanup - // function to prevent hanging refs to unmounted DOM nodes - inputRef.current = null; - }; - }; - return -} -``` - --- ### React event object {/*react-event-object*/} diff --git a/src/content/reference/react/useRef.md b/src/content/reference/react/useRef.md index 8ab53aef371..020d8c0f1a1 100644 --- a/src/content/reference/react/useRef.md +++ b/src/content/reference/react/useRef.md @@ -538,6 +538,307 @@ Here, the `playerRef` itself is nullable. However, you should be able to convinc --- +### Detect DOM changes with a ref {/*detect-dom-changes-with-a-ref*/} + +In some scenarios, you might need to detect changes in the DOM, such as when a component's children are dynamically updated. You can achieve this by using a `ref` callback wrapped in `useCallback` to create a MutationObserver. This approach allows you to observe changes in the DOM and perform actions based on those changes. + + + +```js src/App.js active +import { useState, useRef, useCallback } from "react"; +import { useDrawReactLogo } from "./draw-logo"; + +export default function ReactLogo() { + const [loading, setLoading] = useState(true); + const logoRef = useRef(null); + // the ref callback function should be wraped in + // useCallback so the listener doesn't reconnect + // on each render + const setLogoRef = useCallback((node) => { + logoRef.current = node; + const observer = new MutationObserver(() => { + if (node && node.children.length > 0) { + setLoading(false); + logoRef.current = null; + observer.disconnect(); + } + }); + observer.observe(node, { childList: true }); + + return () => { + // When defining a ref callback cleanup function + // it is important to re-assign the ref object + // to null so that other references will not + // point to the ghost element that no longer exists + logoRef.current = null; + observer.disconnect(); + }; + }, []); + useDrawReactLogo(logoRef); + + return ( +
+ {loading ?
Loading...
: null} +
+
+ ); +} +``` + +```js src/draw-logo.js hidden +import { useRef, useEffect } from "react"; + +export function useDrawReactLogo(chartRef) { + // Use a ref to that status of if drawing + // has started or not outside of render + const drawnRef = useRef(false); + useEffect(() => { + if (!drawnRef.current) { + delayedDrawReactLogo(chartRef.current); + drawnRef.current = true; + } + }, [chartRef]); +} + +function delayedDrawReactLogo(node) { + // add 500ms delay to simulate + // a long drawing time + setTimeout(() => drawReactLogo(node), 500); +} + +function drawReactLogo(node) { + const svgNamespace = "http://www.w3.org/2000/svg"; + const createSvgElement = (type, attributes) => { + const element = document.createElementNS(svgNamespace, type); + Object.entries(attributes).forEach(([key, value]) => { + element.setAttribute(key, value); + }); + return element; + }; + const svg = createSvgElement("svg", { + width: "120", + height: "120", + viewBox: "0 0 100 100", + }); + const ellipses = [{ rotate: 0 }, { rotate: 60 }, { rotate: 120 }]; + ellipses.forEach(({ rotate }) => { + const ellipse = createSvgElement("ellipse", { + cx: "50", + cy: "50", + rx: "35", + ry: "13.75", + transform: `rotate(${rotate}, 50, 50)`, + fill: "none", + stroke: "#58C4DC", + "stroke-width": "3", + }); + svg.appendChild(ellipse); + }); + const circle = createSvgElement("circle", { + cx: "50", + cy: "50", + r: "6.25", + fill: "#58C4DC", + }); + svg.appendChild(circle); + node.appendChild(svg); +} +``` + +
+ + + +#### Prevent reconnections with useCallback {/*prevent-listener-reconnections-with-usecallback*/} + +When a ref callback function change, React will disconnect and reconnect on render. This is similar to a function dependency in an effect. React does this because new prop values may be needed to be passed to the ref callback function. + +```js +export default function ReactLogo() { + const setLogoRef = (node) => { + //... + }; + //... + return
+} +``` + +To avoid unnecessary reconnections wrap your ref callback function in [useCallback](/reference/react/useCallback). Make sure to add any dependancies to the dependency array so the ref callback called with updated props when necessary. + +```js +export default function ReactLogo() { + const setLogoRef = useCallback((node) => { + //.... + }, []); + //... + return
+} +``` + + + +```js +import { useState, useCallback } from "react"; + +function WithoutCallback() { + const [count, setCount] = useState(0); + + // 🚩 without useCallback, the callback changes every + // render, which causes the listener to reconnect + const handleRefEffect = (node) => { + function onClick() { + setCount((count) => count + 1); + } + console.log("without: adding event listener", node); + node.addEventListener("click", onClick); + return () => { + console.log("without: removing event listener", node); + node.removeEventListener("click", onClick); + }; + }; + + return ; +} + +function WithCallback() { + const [count, setCount] = useState(0); + + // ✅ with useCallback, the callback is stable + // so the listener doesn't reconnect each time + const handleRefEffect = useCallback((node) => { + function onClick() { + setCount((count) => count + 1); + } + console.log("with: adding event listener", node); + node.addEventListener("click", onClick); + return () => { + console.log("with: removing event listener", node); + node.removeEventListener("click", onClick); + }; + }, []); + + return ; +} + +export default function App() { + const [count, setCount] = useState(0); + + const handleRefEffect = (node) => { + function onClick() { + setCount((count) => count + 1); + } + console.log("adding event listener", node); + node.addEventListener("click", onClick); + return () => { + console.log("removing event listener", node); + node.removeEventListener("click", onClick); + }; + }; + + return ( + <> +

without useCallback

+ +

with useCallback

+ + + ); +} +``` + +
+ +
+ + + +#### Avoiding Stale Refs {/*avoiding-stale-refs*/} + +A `ref` callback function with a cleanup function that does not set `ref.current` to `null` can result in a `ref` to a unmounted node. Uncheck "Show Input" below and click "Submit" to see how the `ref` to the unmounted `` is still accessible by the click handler for the form. + + + +```js +import { useRef, useState } from "react"; + +export default function MyForm() { + const [showInput, setShowInput] = useState(true); + const inputRef = useRef(); + const handleCheckboxChange = (event) => { + setShowInput(event.target.checked); + }; + const handleSubmit = (event) => { + event.preventDefault(); + if (inputRef.current) { + alert(`Input value is: "${inputRef.current.value}"`); + } else { + alert("no input"); + } + }; + const inputRefCallback = (node) => { + inputRef.current = node; + return () => { + // ⚠️ You must set `ref.current` to `null` + // in this cleanup function e.g. + // `inputRef.current = null;` + // to prevent hanging refs to unmounted DOM nodes + }; + }; + + return ( +
+
+ +
+ {showInput && ( +
+ +
+ )} + +
+ ); +} +``` + +
+ +To fix the hanging ref to the DOM node that is no longer rendered, set `ref.current` to `null` in the `ref` callback cleanup function. + +```js +import { useRef } from "react"; + +function MyInput() { + const inputRef = useRef() + const inputRefCallback = (node) => { + inputRef.current = node; + return () => { + // ⚠️ You must set `ref.current` to `null` in this cleanup + // function to prevent hanging refs to unmounted DOM nodes + inputRef.current = null; + }; + }; + return +} +``` +
+ +--- + ## Troubleshooting {/*troubleshooting*/} ### I can't get a ref to a custom component {/*i-cant-get-a-ref-to-a-custom-component*/} @@ -592,3 +893,6 @@ export default MyInput; Then the parent component can get a ref to it. Read more about [accessing another component's DOM nodes.](/learn/manipulating-the-dom-with-refs#accessing-another-components-dom-nodes) + + + From 405912c2a9552f15fa28c24db8a6a6724ac63f89 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Sat, 21 Dec 2024 00:26:30 -0800 Subject: [PATCH 4/7] remove unnecessary whitespace --- src/content/reference/react/useRef.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/content/reference/react/useRef.md b/src/content/reference/react/useRef.md index 020d8c0f1a1..1f8a1f224f5 100644 --- a/src/content/reference/react/useRef.md +++ b/src/content/reference/react/useRef.md @@ -893,6 +893,3 @@ export default MyInput; Then the parent component can get a ref to it. Read more about [accessing another component's DOM nodes.](/learn/manipulating-the-dom-with-refs#accessing-another-components-dom-nodes) - - - From b9eab90485ab833841282d09c9c3b0a48c1db217 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Sat, 21 Dec 2024 00:30:03 -0800 Subject: [PATCH 5/7] remove useCallback reconnection sandpack example --- src/content/reference/react/useRef.md | 75 +-------------------------- 1 file changed, 1 insertion(+), 74 deletions(-) diff --git a/src/content/reference/react/useRef.md b/src/content/reference/react/useRef.md index 1f8a1f224f5..77c70a199ce 100644 --- a/src/content/reference/react/useRef.md +++ b/src/content/reference/react/useRef.md @@ -663,7 +663,7 @@ export default function ReactLogo() { } ``` -To avoid unnecessary reconnections wrap your ref callback function in [useCallback](/reference/react/useCallback). Make sure to add any dependancies to the dependency array so the ref callback called with updated props when necessary. +To avoid unnecessary reconnections wrap your ref callback function in [useCallback](/reference/react/useCallback). Make sure to add any dependancies to the `useCallback` dependency array. This will ensure the ref callback is called with updated props when necessary. ```js export default function ReactLogo() { @@ -675,79 +675,6 @@ export default function ReactLogo() { } ``` - - -```js -import { useState, useCallback } from "react"; - -function WithoutCallback() { - const [count, setCount] = useState(0); - - // 🚩 without useCallback, the callback changes every - // render, which causes the listener to reconnect - const handleRefEffect = (node) => { - function onClick() { - setCount((count) => count + 1); - } - console.log("without: adding event listener", node); - node.addEventListener("click", onClick); - return () => { - console.log("without: removing event listener", node); - node.removeEventListener("click", onClick); - }; - }; - - return ; -} - -function WithCallback() { - const [count, setCount] = useState(0); - - // ✅ with useCallback, the callback is stable - // so the listener doesn't reconnect each time - const handleRefEffect = useCallback((node) => { - function onClick() { - setCount((count) => count + 1); - } - console.log("with: adding event listener", node); - node.addEventListener("click", onClick); - return () => { - console.log("with: removing event listener", node); - node.removeEventListener("click", onClick); - }; - }, []); - - return ; -} - -export default function App() { - const [count, setCount] = useState(0); - - const handleRefEffect = (node) => { - function onClick() { - setCount((count) => count + 1); - } - console.log("adding event listener", node); - node.addEventListener("click", onClick); - return () => { - console.log("removing event listener", node); - node.removeEventListener("click", onClick); - }; - }; - - return ( - <> -

without useCallback

- -

with useCallback

- - - ); -} -``` - -
- From 8541292c7a786d3715469c418fadbced674fb20a Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Sat, 21 Dec 2024 00:32:26 -0800 Subject: [PATCH 6/7] fix formatting --- src/content/reference/react/useRef.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/reference/react/useRef.md b/src/content/reference/react/useRef.md index 77c70a199ce..4a477151626 100644 --- a/src/content/reference/react/useRef.md +++ b/src/content/reference/react/useRef.md @@ -665,7 +665,7 @@ export default function ReactLogo() { To avoid unnecessary reconnections wrap your ref callback function in [useCallback](/reference/react/useCallback). Make sure to add any dependancies to the `useCallback` dependency array. This will ensure the ref callback is called with updated props when necessary. -```js +```js {2,4} export default function ReactLogo() { const setLogoRef = useCallback((node) => { //.... From a7bce3f867c9c1cbea757816b18b0c2cb872a421 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Mon, 23 Dec 2024 12:20:59 -0800 Subject: [PATCH 7/7] rewrite and move content to useRef reference page --- src/content/reference/react/useRef.md | 205 ++++++++++++++------------ 1 file changed, 114 insertions(+), 91 deletions(-) diff --git a/src/content/reference/react/useRef.md b/src/content/reference/react/useRef.md index 4a477151626..6bf8ef057e8 100644 --- a/src/content/reference/react/useRef.md +++ b/src/content/reference/react/useRef.md @@ -540,41 +540,96 @@ Here, the `playerRef` itself is nullable. However, you should be able to convinc ### Detect DOM changes with a ref {/*detect-dom-changes-with-a-ref*/} -In some scenarios, you might need to detect changes in the DOM, such as when a component's children are dynamically updated. You can achieve this by using a `ref` callback wrapped in `useCallback` to create a MutationObserver. This approach allows you to observe changes in the DOM and perform actions based on those changes. +In some situations, you might need to detect changes in the DOM, such as when a 3rd party library draws visualizations directly to the DOM. To do so, first create a ref callback: a function passed to the ref attribute of the DOM node you want to observe. The ref callback takes a single argument: the DOM node you'd like to observe. Wrap your ref callback in `useCallback` to [prevent unnecessary reconnections](#how-to-avoid-callback-reconnections-with-usecallback). + +```js {5,10} +import { useRef, useCallback } from "react"; + +function Logo() { + const logoRef = useRef(null); + const setLogoRef = useCallback((node) => { + logoRef.current = node; + //... + }, []); + //... + return
+} +``` + +Next, set up a [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) to monitor changes and handle those changes accordingly in your ref callback. In this example, the MutationObserver is monitoring for changes to the children of a `
`. Don't forget to disconnect your observer when you no longer need to monitor for changes. + +```js {7-13} +import { useRef, useCallback } from "react"; + +function Logo() { + const logoRef = useRef(null); + const setLogoRef = useCallback((node) => { + logoRef.current = node; + const observer = new MutationObserver(() => { + if (node && node.children.length > 0) { + // TODO: handle when children are added to this DOM node + observer.disconnect(); + } + }); + observer.observe(node, { childList: true }); + }, []); + //... + return
+} +``` + +Lastly, you'll need to return a function from your ref callback to cleanup the observer and ref. Explicitly setting the ref to null during cleanup prevents [refs to unmounted DOM nodes](#how-to-avoid-a-ref-to-a-unmounted-node). + +```js {10-13} +import { useRef, useCallback } from "react"; + +function Logo() { + const logoRef = useRef(null); + const setLogoRef = useCallback((node) => { + logoRef.current = node; + const observer = new MutationObserver(() => { /*...*/ }); + observer.observe(node, { /*...*/ }); + + return () => { + logoRef.current = null; + observer.disconnect(); + }; + }, []); + //... + return
+} +``` + +In this example, the `Logo` component utilizes a `MutationObserver` to detect when child elements are added to a `
` allowing it to update the component's state and stop displaying a loading indicator once the logo is fully drawn. Tap the "Reset" button in the upper right corner of the CodeSandbox example below to see how the loading indicator is replaced by the logo. ```js src/App.js active import { useState, useRef, useCallback } from "react"; -import { useDrawReactLogo } from "./draw-logo"; +import { useDrawLogo } from "./draw-logo"; -export default function ReactLogo() { +export default function Logo() { const [loading, setLoading] = useState(true); const logoRef = useRef(null); - // the ref callback function should be wraped in - // useCallback so the listener doesn't reconnect - // on each render + // useCallback prevents reconnections on each render const setLogoRef = useCallback((node) => { logoRef.current = node; const observer = new MutationObserver(() => { if (node && node.children.length > 0) { setLoading(false); - logoRef.current = null; observer.disconnect(); } }); observer.observe(node, { childList: true }); return () => { - // When defining a ref callback cleanup function - // it is important to re-assign the ref object - // to null so that other references will not - // point to the ghost element that no longer exists + // Explicitly setting the ref to null in cleanup + // prevents refs to unmounted DOM nodes logoRef.current = null; observer.disconnect(); }; }, []); - useDrawReactLogo(logoRef); + useDrawLogo(logoRef); return (
@@ -588,25 +643,25 @@ export default function ReactLogo() { ```js src/draw-logo.js hidden import { useRef, useEffect } from "react"; -export function useDrawReactLogo(chartRef) { - // Use a ref to that status of if drawing +export function useDrawLogo(ref) { + // Use a ref to store the status of if drawing // has started or not outside of render const drawnRef = useRef(false); useEffect(() => { if (!drawnRef.current) { - delayedDrawReactLogo(chartRef.current); + delayedDrawLogo(ref.current); drawnRef.current = true; } - }, [chartRef]); + }, [ref]); } -function delayedDrawReactLogo(node) { +function delayedDrawLogo(node) { // add 500ms delay to simulate // a long drawing time - setTimeout(() => drawReactLogo(node), 500); + setTimeout(() => drawLogo(node), 500); } -function drawReactLogo(node) { +function drawLogo(node) { const svgNamespace = "http://www.w3.org/2000/svg"; const createSvgElement = (type, attributes) => { const element = document.createElementNS(svgNamespace, type); @@ -649,12 +704,14 @@ function drawReactLogo(node) { -#### Prevent reconnections with useCallback {/*prevent-listener-reconnections-with-usecallback*/} +#### How to avoid callback reconnections with useCallback {/*how-to-avoid-callback-reconnections-with-usecallback*/} -When a ref callback function change, React will disconnect and reconnect on render. This is similar to a function dependency in an effect. React does this because new prop values may be needed to be passed to the ref callback function. +When React re-renders a component, all the functions defined in the component are recreated. This includes ref callback functions defined in components. When a ref callback function is changed or recreated, React will disconnect and reconnect your ref callback function. React does this because new prop values may need to be passed to the ref callback function. This is similar to a function dependency in an effect. -```js +```js {4} export default function ReactLogo() { + // 🚩 without useCallback, the callback changes every + // render, which causes the listener to reconnect const setLogoRef = (node) => { //... }; @@ -663,10 +720,14 @@ export default function ReactLogo() { } ``` -To avoid unnecessary reconnections wrap your ref callback function in [useCallback](/reference/react/useCallback). Make sure to add any dependancies to the `useCallback` dependency array. This will ensure the ref callback is called with updated props when necessary. +To disconnect, React will call your ref callback function with `null` as an argument. To reconnect, React calls your ref callback function with the DOM node as an argument. + +To avoid unnecessary disconnections and reconnections wrap your ref callback function in [useCallback](/reference/react/useCallback). Make sure to add any dependencies to the `useCallback` dependency array. This will ensure the ref callback is called with updated props when necessary. -```js {2,4} +```js {4,6} export default function ReactLogo() { + // ✅ with useCallback, the callback is stable + // so the listener doesn't reconnect each render const setLogoRef = useCallback((node) => { //.... }, []); @@ -679,89 +740,51 @@ export default function ReactLogo() { -#### Avoiding Stale Refs {/*avoiding-stale-refs*/} +#### How to avoid a ref to a unmounted node {/*how-to-avoid-a-ref-to-a-unmounted-node*/} -A `ref` callback function with a cleanup function that does not set `ref.current` to `null` can result in a `ref` to a unmounted node. Uncheck "Show Input" below and click "Submit" to see how the `ref` to the unmounted `` is still accessible by the click handler for the form. - - +A `ref` callback function with a cleanup function that does not set `ref.current` to `null` can result in a `ref` to a unmounted node. ```js -import { useRef, useState } from "react"; +export default function Logo() { + const logoRef = useRef(null); + const setLogoRef = useCallback((node) => { + logoRef.current = node; + //... -export default function MyForm() { - const [showInput, setShowInput] = useState(true); - const inputRef = useRef(); - const handleCheckboxChange = (event) => { - setShowInput(event.target.checked); - }; - const handleSubmit = (event) => { - event.preventDefault(); - if (inputRef.current) { - alert(`Input value is: "${inputRef.current.value}"`); - } else { - alert("no input"); - } - }; - const inputRefCallback = (node) => { - inputRef.current = node; + // 🚩 if your ref cleanup function does not explicitly + // set the ref to null the ref may point to a + // unmounted DOM node return () => { - // ⚠️ You must set `ref.current` to `null` - // in this cleanup function e.g. - // `inputRef.current = null;` - // to prevent hanging refs to unmounted DOM nodes + observer.disconnect(); }; - }; - - return ( -
-
- -
- {showInput && ( -
- -
- )} - -
- ); + }, []); + //... + return
} ``` -
- To fix the hanging ref to the DOM node that is no longer rendered, set `ref.current` to `null` in the `ref` callback cleanup function. -```js -import { useRef } from "react"; +```js {11} +export default function Logo() { + const logoRef = useRef(null); + const setLogoRef = useCallback((node) => { + logoRef.current = node; + //... -function MyInput() { - const inputRef = useRef() - const inputRefCallback = (node) => { - inputRef.current = node; return () => { - // ⚠️ You must set `ref.current` to `null` in this cleanup - // function to prevent hanging refs to unmounted DOM nodes - inputRef.current = null; + // ✅ Explicitly setting the ref to null in the + // cleanup function prevents references to + // unmounted DOM nodes + logoRef.current = null; + observer.disconnect(); }; - }; - return + }, []); + //... + return
} ``` +
---