Skip to main content

React.StrictMode

React.StrictMode - це обгортка тільки для розробки, яка двічі викликає рендери та ефекти, щоб виявити побічні ефекти та небезпечні патерни до того, як вони потраплять у продакшн.

Теорія

TL;DR

  • Уяви авіасимулятор: StrictMode навантажує компоненти двічі під час розробки, щоб знайти слабкі місця до реальних користувачів
  • Рендерить компоненти двічі та запускає ефекти за схемою setup-cleanup-setup тільки в dev; продакшн-білди StrictMode повністю прибирає через tree-shaking
  • React 18+ додав подвійний монтаж ефектів, щоб знайти баги в cleanup, які проявляються в конкурентних фічах
  • Новий проект: огорни <App /> одразу. Легасі-код: додавай StrictMode по одному піддереву.

Швидкий приклад

jsx
// index.js import React, { useState, useEffect } from 'react'; import { createRoot } from 'react-dom/client'; function Counter() { const [count, setCount] = useState(0); console.log('Render'); // Виводиться двічі в dev useEffect(() => { console.log('Effect ran'); // Виводиться двічі при монтажі в dev }); return <button onClick={() => setCount(c => c + 1)}>{count}</button>; } createRoot(document.getElementById('root')).render( <React.StrictMode> <Counter /> </React.StrictMode> ); // Dev: "Render" x2, "Effect ran" x2 // Продакшн: "Render" x1, "Effect ran" x1

Подвійні логи - це не баг. Це саме те, для чого StrictMode існує.

Що перевіряє StrictMode

StrictMode виявляє чотири категорії проблем:

  • Нечисті функції рендеру: будь-яка мутація стану або зовнішніх значень під час рендеру спрацьовує двічі, що робить мутацію очевидною
  • Застарілі методи життєвого циклу: componentWillMount, componentWillReceiveProps, componentWillUpdate - всі видають попередження в консолі
  • Застарілі рядкові refs: стиль ref="button", deprecated з React 16 і прибраний у React 19
  • Неідемпотентні ефекти (React 18+): setup і cleanup ефекту запускаються в послідовності (setup, cleanup, setup), щоб перевірити, чи cleanup правильно скасовує те, що зробив setup

Як працює подвійний виклик

Fiber reconciler React огортає цільові компоненти в спеціальний dev-шлях всередині ReactFiberStrictMode.js. В режимі розробки він двічі викликає render, ініціалізатори useState, колбеки useMemo та setup/cleanup useEffect. DOM-мутації другого виклику відкидаються до того, як досягнуть браузера. Fiber-дерево спільне для обох проходів; React повторює виклики функцій і викидає результат другого проходу. Для користувача нічого видимого не змінюється.

Для useEffect у React 18+ послідовність при монтажі така: setup запускається, cleanup запускається, setup запускається знову. Це симулює поведінку конкурентних фіч, як-от Transitions, де React може демонтувати і перемонтувати компонент. Якщо cleanup не скасовує те, що зробив setup, третій виклик ламається під час розробки, а не в продакшні.

Ключова різниця від продакшну

StrictMode не змінює того, що рендерять компоненти, і як додаток поводиться для користувачів. Він змінює тільки те, скільки разів React внутрішньо викликає функції під час розробки. Продакшн-білди прибирають обгортку StrictMode повністю, тому подвійні виклики не відбуваються поза dev.

Коли використовувати

  • Новий проект на React 18+: огорни <App /> одразу. Vite і Create React App роблять це за замовчуванням.
  • Міграція легасі-коду: додавай StrictMode до одного піддерева за раз, виправляй попередження, розширюй покриття.
  • Стороння бібліотека, яка спамить попередженнями, які ти не можеш виправити: залиш цей компонент за межами обгортки і повідом авторів бібліотеки.
jsx
// Вибіркова обгортка - Header і LegacyWidget залишаються зовні function App() { return ( <div> <Header /> <React.StrictMode> <Sidebar /> <Content /> </React.StrictMode> <LegacyWidget /> </div> ); }

Типові помилки

Сприймати подвійні логи як баг. Dev-консоль показує "Render" двічі. Це правильно. Продакшн показує один раз. Виправлення - не прибирати StrictMode.

Мутувати спільні об'єкти під час рендеру. Саме для цього StrictMode і існує.

jsx
// Баг: спільний об'єкт мутується під час рендеру const config = { count: 0 }; function BadComponent() { config.count++; // Запускається двічі в dev, тому count = 2 після першого видимого рендеру return <div>{config.count}</div>; } // Виправлення: значення береться зі стану function GoodComponent() { const [count, setCount] = useState(0); return <div>{count}</div>; }

Ефекти без правильного cleanup. React 18+ StrictMode запускає cleanup перед другим setup. Неповний cleanup ламає другий прохід.

jsx
// Баг: два інтервали запускаються при монтажі в dev, бо cleanup відсутній useEffect(() => { const id = setInterval(() => setCount(c => c + 1), 1000); // немає cleanup }, []); // Виправлення: cleanup робить ефект ідемпотентним useEffect(() => { const id = setInterval(() => setCount(c => c + 1), 1000); return () => clearInterval(id); }, []);

Прибирати StrictMode, щоб замовкнути попередження сторонніх бібліотек. Це ховає реальні проблеми замість їх вирішення. Перенеси "шумну" бібліотеку за межі обгортки і залиш баг-репорт авторам.

Розраховувати на поведінку StrictMode в Jest-тестах. Jest рендерить компоненти один раз за замовчуванням. Щоб тестувати в умовах StrictMode, огорни явно.

jsx
// setupTests.js import React from 'react'; import { render } from '@testing-library/react'; export function renderStrict(ui) { return render(<React.StrictMode>{ui}</React.StrictMode>); }

Де зустрічається

  • Vite + React і Create React App: обидва шаблони за замовчуванням огортають корінь у <StrictMode>
  • Next.js: підключається через reactStrictMode: true в next.config.js; за замовчуванням вимкнено
  • Storybook: огортай stories у StrictMode, щоб знаходити проблеми з HOC раніше
  • Redux Toolkit: подвійні виклики безпечні, бо RTK-селектори є чистими функціями за дизайном

В кодобазах часто зустрічається такий патерн: fetch спрацьовує двічі в dev і команда прибирає StrictMode замість того, щоб додати AbortController. Додавання контролера виправляє і поведінку в StrictMode, і реальний memory leak, який вже існував у продакшні, але був непомітний.

Питання на співбесіді

Q: Чому React 18+ StrictMode запускає ефекти двічі при монтажі?
A: Це симулює конкурентні фічі, де React може демонтувати і перемонтувати компонент для звільнення ресурсів. Якщо setup запускається двічі без того, щоб cleanup нейтралізував перший запуск, в конкурентному режимі виникають реальні баги.

Q: Чи гальмує StrictMode продакшн?
A: Ні. React повністю прибирає його через tree-shaking у продакшні. Подвійні виклики існують тільки в dev-білдах.

Q: Стороння бібліотека видає попередження в StrictMode. Що робити?
A: Перевір, чи є новіша версія бібліотеки з підтримкою React 18. Якщо ні, перенеси цей компонент за межі StrictMode і залиш баг-репорт авторам. Не вимикай StrictMode для всього додатку.

Q: Що замінило рядкові refs?
A: useRef у функціональних компонентах, React.createRef() у класових. Рядкові refs прибрані в React 19.

Q (senior): Як StrictMode взаємодіє з React Compiler у canary-білдах?
A: React Compiler автоматично мемоїзує компоненти, вважаючи рендери чистими. Подвійний виклик StrictMode перевіряє це припущення: якщо компонент повертає різний результат на другому рендері, компілятор не може безпечно кешувати його. Так StrictMode виявляє нечисті функції, які компілятор інакше міг би непомітно оптимізувати неправильно.

Приклади

Чистий проти нечистого рендеру під StrictMode

jsx
// Чиста функція: виживає при подвійному рендері, обидва логи однакові function Greeting({ name }) { const msg = `Hello, ${name}`; // обчислюється тільки з props console.log(msg); // "Hello, World" x2 в dev - обидва однакові return <div>{msg}</div>; } // Нечиста функція: подвійний рендер виявляє мутацію let calls = 0; function ImpureGreeting({ name }) { calls++; // мутує зовнішню змінну під час рендеру console.log(`Call #${calls}: Hello, ${name}`); // Dev: "Call #1: Hello, World" потім "Call #2: Hello, World" // calls = 2 після одного видимого рендеру - справжній баг return <div>Hello, {name}</div>; }

Greeting виводить однаковий рядок двічі, бо функція чиста. ImpureGreeting робить мутацію очевидною: calls дорівнює 2 після того, що користувач бачить як один рендер. Саме для цього все і затівалось.

Fetch з cleanup через AbortController

jsx
// UserList.jsx import { useState, useEffect } from 'react'; function UserList() { const [users, setUsers] = useState([]); useEffect(() => { const controller = new AbortController(); fetch('https://jsonplaceholder.typicode.com/users', { signal: controller.signal, }) .then(res => res.json()) .then(data => setUsers(data)) .catch(err => { if (err.name !== 'AbortError') console.error(err); }); return () => controller.abort(); // cleanup скасовує перший запит }, []); return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>; }

Без AbortController StrictMode запускає два запити при монтажі в dev і обидва намагаються викликати setUsers. З cleanup перший запит скасовується і завершується тільки другий. У продакшні - один запит. Той самий код, правильна поведінка в обох середовищах.

Застарілі методи життєвого циклу - до і після

jsx
// До: три методи викликають попередження StrictMode class UserProfile extends React.Component { componentWillMount() { // Попередження: використовуй componentDidMount this.setState({ loading: true }); } componentWillReceiveProps(nextProps) { // Попередження: використовуй getDerivedStateFromProps if (nextProps.userId !== this.props.userId) { this.setState({ loading: true }); } } render() { return <div>{this.state.loading ? 'Loading...' : this.props.userId}</div>; } } // Після: класовий компонент без попереджень StrictMode class UserProfile extends React.Component { static getDerivedStateFromProps(props, state) { if (props.userId !== state.prevUserId) { return { loading: true, prevUserId: props.userId }; } return null; } componentDidMount() { this.loadUser(this.props.userId); } componentDidUpdate(prevProps) { if (prevProps.userId !== this.props.userId) { this.loadUser(this.props.userId); } } loadUser(id) { // логіка завантаження this.setState({ loading: false }); } render() { return <div>{this.state.loading ? 'Loading...' : this.props.userId}</div>; } }

componentWillMount і componentWillReceiveProps позначені як UNSAFE_ з React 16.3 і будуть прибрані в наступній мажорній версії. StrictMode робить це видимим зараз, щоб ти встиг мігрувати до дедлайну.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?