diff --git a/src/content/learn/manipulating-the-dom-with-refs.md b/src/content/learn/manipulating-the-dom-with-refs.md index a0a82266783..b8230f75d3d 100644 --- a/src/content/learn/manipulating-the-dom-with-refs.md +++ b/src/content/learn/manipulating-the-dom-with-refs.md @@ -341,6 +341,14 @@ In this example, `itemsRef` doesn't hold a single DOM node. Instead, it holds a This lets you read individual DOM nodes from the Map later. + + +When Strict Mode is enabled, ref callbacks will run twice in development. + +Read more about [how this helps find bugs](/reference/react/StrictMode#fixing-bugs-found-by-re-running-ref-callbacks-in-development) in callback refs. + + + ## Accessing another component's DOM nodes {/*accessing-another-components-dom-nodes*/} diff --git a/src/content/reference/react-dom/components/common.md b/src/content/reference/react-dom/components/common.md index f0119972d99..9d15332139d 100644 --- a/src/content/reference/react-dom/components/common.md +++ b/src/content/reference/react-dom/components/common.md @@ -280,7 +280,7 @@ To support backwards compatibility, if a cleanup function is not returned from t #### Caveats {/*caveats*/} * 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 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. +* 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. --- diff --git a/src/content/reference/react/StrictMode.md b/src/content/reference/react/StrictMode.md index 43073e8112a..8cf723e7f40 100644 --- a/src/content/reference/react/StrictMode.md +++ b/src/content/reference/react/StrictMode.md @@ -44,6 +44,7 @@ Strict Mode enables the following development-only behaviors: - Your components will [re-render an extra time](#fixing-bugs-found-by-double-rendering-in-development) to find bugs caused by impure rendering. - Your components will [re-run Effects an extra time](#fixing-bugs-found-by-re-running-effects-in-development) to find bugs caused by missing Effect cleanup. +- Your components will [re-run refs callbacks an extra time](#fixing-bugs-found-by-re-running-ref-callbacks-in-development) to find bugs caused by missing ref cleanup. - Your components will [be checked for usage of deprecated APIs.](#fixing-deprecation-warnings-enabled-by-strict-mode) #### Props {/*props*/} @@ -87,6 +88,7 @@ Strict Mode enables the following checks in development: - Your components will [re-render an extra time](#fixing-bugs-found-by-double-rendering-in-development) to find bugs caused by impure rendering. - Your components will [re-run Effects an extra time](#fixing-bugs-found-by-re-running-effects-in-development) to find bugs caused by missing Effect cleanup. +- Your components will [re-run ref callbacks an extra time](#fixing-bugs-found-by-cleaning-up-and-re-attaching-dom-refs-in-development) to find bugs caused by missing ref cleanup. - Your components will [be checked for usage of deprecated APIs.](#fixing-deprecation-warnings-enabled-by-strict-mode) **All of these checks are development-only and do not impact the production build.** @@ -825,7 +827,448 @@ Without Strict Mode, it was easy to miss that your Effect needed cleanup. By run [Read more about implementing Effect cleanup.](/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development) --- +### Fixing bugs found by re-running ref callbacks in development {/*fixing-bugs-found-by-re-running-ref-callbacks-in-development*/} +Strict Mode can also help find bugs in [callbacks refs.](/learn/manipulating-the-dom-with-refs) + +Every callback `ref` has some setup code and may have some cleanup code. Normally, React calls setup when the element is *created* (is added to the DOM) and calls cleanup when the element is *removed* (is removed from the DOM). + +When Strict Mode is on, React will also run **one extra setup+cleanup cycle in development for every callback `ref`.** This may feel surprising, but it helps reveal subtle bugs that are hard to catch manually. + +Consider this example, which allows you to select an animal and then scroll to one of them. Notice when you switch from "Cats" to "Dogs", the console logs show that the number of animals in the list keeps growing, and the "Scroll to" buttons stop working: + + + +```js src/index.js +import { createRoot } from 'react-dom/client'; +import './styles.css'; + +import App from './App'; + +const root = createRoot(document.getElementById("root")); +// ❌ Not using StrictMode. +root.render(); +``` + +```js src/App.js active +import { useRef, useState } from "react"; + +export default function AnimalFriends() { + const itemsRef = useRef([]); + const [animalList, setAnimalList] = useState(setupAnimalList); + const [animal, setAnimal] = useState('cat'); + + function scrollToAnimal(index) { + const list = itemsRef.current; + const {node} = list[index]; + node.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "center", + }); + } + + const animals = animalList.filter(a => a.type === animal) + + return ( + <> + +
+ +
+
    + {animals.map((animal) => ( +
  • { + const list = itemsRef.current; + const item = {animal: animal, node}; + list.push(item); + console.log(`✅ Adding animal to the map. Total animals: ${list.length}`); + if (list.length > 10) { + console.log('❌ Too many animals in the list!'); + } + return () => { + // 🚩 No cleanup, this is a bug! + } + }} + > + +
  • + ))} + +
+
+ + ); +} + +function setupAnimalList() { + const animalList = []; + for (let i = 0; i < 10; i++) { + animalList.push({type: 'cat', src: "https://loremflickr.com/320/240/cat?lock=" + i}); + } + for (let i = 0; i < 10; i++) { + animalList.push({type: 'dog', src: "https://loremflickr.com/320/240/dog?lock=" + i}); + } + + return animalList; +} + +``` + +```css +div { + width: 100%; + overflow: hidden; +} + +nav { + text-align: center; +} + +button { + margin: .25rem; +} + +ul, +li { + list-style: none; + white-space: nowrap; +} + +li { + display: inline; + padding: 0.5rem; +} +``` + +```json package.json hidden +{ + "dependencies": { + "react": "beta", + "react-dom": "beta", + "react-scripts": "^5.0.0" + } +} +``` + +
+ + +**This is a production bug!** Since the ref callback doesn't remove animals from the list in the cleanup, the list of animals keeps growing. This is a memory leak that can cause performance problems in a real app, and breaks the behavior of the app. + +The issue is the ref callback doesn't cleanup after itself: + +```js {6-8} +
  • { + const list = itemsRef.current; + const item = {animal, node}; + list.push(item); + return () => { + // 🚩 No cleanup, this is a bug! + } + }} +
  • +``` + +Now let's wrap the original (buggy) code in ``: + + + +```js src/index.js +import { createRoot } from 'react-dom/client'; +import {StrictMode} from 'react'; +import './styles.css'; + +import App from './App'; + +const root = createRoot(document.getElementById("root")); +// ✅ Using StrictMode. +root.render( + + + +); +``` + +```js src/App.js active +import { useRef, useState } from "react"; + +export default function AnimalFriends() { + const itemsRef = useRef([]); + const [animalList, setAnimalList] = useState(setupAnimalList); + const [animal, setAnimal] = useState('cat'); + + function scrollToAnimal(index) { + const list = itemsRef.current; + const {node} = list[index]; + node.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "center", + }); + } + + const animals = animalList.filter(a => a.type === animal) + + return ( + <> + +
    + +
    +
      + {animals.map((animal) => ( +
    • { + const list = itemsRef.current; + const item = {animal: animal, node} + list.push(item); + console.log(`✅ Adding animal to the map. Total animals: ${list.length}`); + if (list.length > 10) { + console.log('❌ Too many animals in the list!'); + } + return () => { + // 🚩 No cleanup, this is a bug! + } + }} + > + +
    • + ))} + +
    +
    + + ); +} + +function setupAnimalList() { + const animalList = []; + for (let i = 0; i < 10; i++) { + animalList.push({type: 'cat', src: "https://loremflickr.com/320/240/cat?lock=" + i}); + } + for (let i = 0; i < 10; i++) { + animalList.push({type: 'dog', src: "https://loremflickr.com/320/240/dog?lock=" + i}); + } + + return animalList; +} + +``` + +```css +div { + width: 100%; + overflow: hidden; +} + +nav { + text-align: center; +} + +button { + margin: .25rem; +} + +ul, +li { + list-style: none; + white-space: nowrap; +} + +li { + display: inline; + padding: 0.5rem; +} +``` + +```json package.json hidden +{ + "dependencies": { + "react": "beta", + "react-dom": "beta", + "react-scripts": "^5.0.0" + } +} +``` + +
    + +**With Strict Mode, you immediately see that there is a problem**. Strict Mode runs an extra setup+cleanup cycle for every callback ref. This callback ref has no cleanup logic, so it adds refs but doesn't remove them. This is a hint that you're missing a cleanup function. + +Strict Mode lets you eagerly find mistakes in callback refs. When you fix your callback by adding a cleanup function in Strict Mode, you *also* fix many possible future production bugs like the "Scroll to" bug from before: + + + +```js src/index.js +import { createRoot } from 'react-dom/client'; +import {StrictMode} from 'react'; +import './styles.css'; + +import App from './App'; + +const root = createRoot(document.getElementById("root")); +// ✅ Using StrictMode. +root.render( + + + +); +``` + +```js src/App.js active +import { useRef, useState } from "react"; + +export default function AnimalFriends() { + const itemsRef = useRef([]); + const [animalList, setAnimalList] = useState(setupAnimalList); + const [animal, setAnimal] = useState('cat'); + + function scrollToAnimal(index) { + const list = itemsRef.current; + const {node} = list[index]; + node.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "center", + }); + } + + const animals = animalList.filter(a => a.type === animal) + + return ( + <> + +
    + +
    +
      + {animals.map((animal) => ( +
    • { + const list = itemsRef.current; + const item = {animal, node}; + list.push({animal: animal, node}); + console.log(`✅ Adding animal to the map. Total animals: ${list.length}`); + if (list.length > 10) { + console.log('❌ Too many animals in the list!'); + } + return () => { + list.splice(list.indexOf(item)); + console.log(`❌ Removing animal from the map. Total animals: ${itemsRef.current.length}`); + } + }} + > + +
    • + ))} + +
    +
    + + ); +} + +function setupAnimalList() { + const animalList = []; + for (let i = 0; i < 10; i++) { + animalList.push({type: 'cat', src: "https://loremflickr.com/320/240/cat?lock=" + i}); + } + for (let i = 0; i < 10; i++) { + animalList.push({type: 'dog', src: "https://loremflickr.com/320/240/dog?lock=" + i}); + } + + return animalList; +} + +``` + +```css +div { + width: 100%; + overflow: hidden; +} + +nav { + text-align: center; +} + +button { + margin: .25rem; +} + +ul, +li { + list-style: none; + white-space: nowrap; +} + +li { + display: inline; + padding: 0.5rem; +} +``` + +```json package.json hidden +{ + "dependencies": { + "react": "beta", + "react-dom": "beta", + "react-scripts": "^5.0.0" + } +} +``` + +
    + +Now on inital mount in StrictMode, the ref callbacks are all setup, cleaned up, and setup again: + +``` +... +✅ Adding animal to the map. Total animals: 10 +... +❌ Removing animal from the map. Total animals: 0 +... +✅ Adding animal to the map. Total animals: 10 +``` + +**This is expected.** Strict Mode confirms that the ref callbacks are cleaned up correctly, so the size never grows above the expected amount. After the fix, there are no memory leaks, and all the features work as expected. + +Without Strict Mode, it was easy to miss the bug until you clicked around to app to notice broken features. Strict Mode made the bugs appear right away, before you push them to production. + +--- ### Fixing deprecation warnings enabled by Strict Mode {/*fixing-deprecation-warnings-enabled-by-strict-mode*/} React warns if some component anywhere inside a `` tree uses one of these deprecated APIs: