From f930da4ab77ebc0b2724818316d80895610842a0 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Thu, 9 May 2024 13:30:45 -0400 Subject: [PATCH] Expand usage example and remove badges --- .../learn/manipulating-the-dom-with-refs.md | 4 +- .../reference/react-dom/components/common.md | 12 + src/content/reference/react/StrictMode.md | 447 +++++++++++++++++- 3 files changed, 453 insertions(+), 10 deletions(-) diff --git a/src/content/learn/manipulating-the-dom-with-refs.md b/src/content/learn/manipulating-the-dom-with-refs.md index e8f8256a8d0..b8230f75d3d 100644 --- a/src/content/learn/manipulating-the-dom-with-refs.md +++ b/src/content/learn/manipulating-the-dom-with-refs.md @@ -343,7 +343,9 @@ This lets you read individual DOM nodes from the Map later. -When [Strict Mode](/reference/react/StrictMode) is on in React 19, [React detaches and re-attaches DOM refs](/reference/react/StrictMode#fixing-bugs-found-by-cleaning-up-and-re-attaching-dom-refs-in-development). This will stress-test callbacks like the one above by calling its cleanup function before calling the callback a second time. +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. diff --git a/src/content/reference/react-dom/components/common.md b/src/content/reference/react-dom/components/common.md index 9d15332139d..10f67d8c903 100644 --- a/src/content/reference/react-dom/components/common.md +++ b/src/content/reference/react-dom/components/common.md @@ -1200,3 +1200,15 @@ input { margin-left: 10px; } ``` + +--- + +## Troubleshooting {/*troubleshooting*/} + +### My callback ref runs twice when the component mounts {/*my-callback-ref-runs-twice-when-the-component-mounts*/} + +When Strict Mode is on, in development, React runs setup and cleanup one extra time before the actual setup. + +This is a stress-test that verifies your ref callback logic is implemented correctly. If this causes visible issues, your cleanup function is missing some logic. The cleanup function should stop or undo whatever the setup function was doing. The rule of thumb is that the user shouldn’t be able to distinguish between the setup being called once (as in production) and a setup → cleanup → setup sequence (as in development). + +Read more about how this helps find bugs and how to fix your logic. \ No newline at end of file diff --git a/src/content/reference/react/StrictMode.md b/src/content/reference/react/StrictMode.md index 002bc22c6c6..8cf723e7f40 100644 --- a/src/content/reference/react/StrictMode.md +++ b/src/content/reference/react/StrictMode.md @@ -44,7 +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 [detach and re-attach refs to components](#fixing-bugs-found-by-cleaning-up-and-re-attaching-dom-refs-in-development) to find bugs caused by missing ref 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*/} @@ -88,7 +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 [detach and re-attach refs to components](#fixing-bugs-found-by-cleaning-up-and-re-attaching-dom-refs-in-development) to find bugs caused by missing ref 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.** @@ -827,17 +827,446 @@ 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 detaching and re-attaching refs to components in development {/*fixing-bugs-found-by-cleaning-up-and-re-attaching-dom-refs-in-development*/} +### Fixing bugs found by re-running ref callbacks in development {/*fixing-bugs-found-by-re-running-ref-callbacks-in-development*/} - -In React 19, React will run an extra setup+cleanup cycle in development for refs to components, much like it does for Effects. +Strict Mode can also help find bugs in [callbacks refs.](/learn/manipulating-the-dom-with-refs) -React will detach refs to components that were created via `useRef` by setting `ref.current` to `null` before setting it to the DOM node or handle. +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). -For [`ref` callbacks](/reference/react-dom/components/common#ref-callback), React will call the callback function with the DOM node or handle as its argument. It will then call the callback's [cleanup function](reference/react-dom/components/common#returns) before calling the `ref` callback function again with the DOM node as its argument. +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. -You can read more about refs in [Manipulating the DOM with Refs.](/learn/manipulating-the-dom-with-refs) - +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*/}