From 17a31786e104b7317d3d32394655b1883526bba1 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Sat, 21 Dec 2024 00:23:32 -0800 Subject: [PATCH] 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) + + +