Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Незмінність у стані React». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Незмінність (immutability) у стані React** означає, що для оновлення стану завжди створюється новий об'єкт або масив. React порівнює посилання через `Object.is()`, а не значення. Те саме посилання - ре-рендеру не буде. ```tsx user.age = 26; setUser(user); // ❌ те саме посилання, пропущено setUser({ ...user, age: 26 }); // ✅ нове посилання, ре-рендер відбудеться ``` **Ключове:** React пропускає оновлення, коли посилання не змінилось.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Незмінність (immutability) у стані React** означає, що при оновленні стану завжди створюються нові об'єкти чи масиви замість змін наявних, бо React порівнює посилання, а не значення всередині. ## Теорія ### TL;DR - React використовує `Object.is()` (порівняння посилань), щоб перевірити чи змінився стан. Не глибоке порівняння. - Якщо мутувати стан напряму, посилання залишається тим самим. React не бачить зміни і пропускає ре-рендер. - Створюй нові об'єкти через spread: `{ ...obj, key: newValue }`. Нові масиви через `map`, `filter` або spread. - Правило вибору: якщо пишеш `state.щось = значення`, це мутація. Так не спрацює. ### Швидкий приклад ```tsx const [user, setUser] = useState({ name: "Alice", age: 25 }); // ❌ Неправильно - мутація на місці, те саме посилання, немає ре-рендеру user.age = 26; setUser(user); // ✅ Правильно - новий об'єкт, нове посилання, React перерендерить setUser({ ...user, age: 26 }); // Компонент ре-рендериться, відображає age: 26 ``` Spread-оператор створює новий об'єкт у пам'яті. React порівнює старе і нове посилання, бачить різницю і ставить ре-рендер у чергу. ### Як React виявляє зміни стану React зберігає посилання на стан у внутрішньому fiber tree. Кожен виклик `setState` запускає порівняння: `Object.is(prevState, nextState)`. Якщо повертає `true`, React повністю пропускає функцію рендеру компонента. Якщо `false`, планує ре-рендер і оновлює DOM. Саме тому `setUser(user)` після мутації `user.age` нічого не робить. Змінна `user` досі вказує на той самий об'єкт у пам'яті. React перевіряє, отримує `true`, іде далі. Цей механізм також пояснює, чому `React.memo`, `useMemo` і `useCallback` працюють так, як вони працюють. Всі три будуються на тому самому порівнянні посилань. ### Коли який підхід використовувати - Оновити одне поле об'єкта: `setUser({ ...user, age: 26 })` - Оновити вкладений об'єкт: spread на кожному рівні, `setState({ ...state, user: { ...state.user, name: "Bob" } })` - Додати елемент до масиву: `setItems([...items, newItem])` - Видалити елемент з масиву: `setItems(items.filter(item => item.id !== id))` - Оновити один елемент у масиві: `setItems(items.map(item => item.id === id ? { ...item, ...changes } : item))` - Глибоко вкладений стан з багатьма рівнями: Immer ### Довідник незмінних операцій | Операція | Мутуюча | Незмінна | |---|---|---| | Додати до масиву | `push()`, `unshift()` | `[...arr, item]` | | Видалити з масиву | `splice()`, `pop()` | `filter()` | | Замінити в масиві | `arr[i] = x` | `map()` | | Сортувати масив | `sort()` | `[...arr].sort()` | | Оновити поле об'єкта | `obj.key = val` | `{ ...obj, key: val }` | | Видалити властивість | `delete obj.key` | `const { key, ...rest } = obj` | ### Типові помилки **1. Мутація вкладеного об'єкта** ```tsx // ❌ Неправильно - зовнішнє посилання не змінилось, ре-рендеру не буде state.user.name = "Bob"; setState(state); // ✅ Правильно - новий зовнішній об'єкт і новий вкладений setState({ ...state, user: { ...state.user, name: "Bob" } }); ``` React перевіряє посилання зовнішнього об'єкта. Воно не змінилось. Ре-рендер пропущено. **2. Методи масиву, що мутують** ```tsx // ❌ Неправильно - push() змінює оригінал, посилання те саме items.push(newItem); setItems(items); // ✅ Правильно - нове посилання на масив setItems([...items, newItem]); ``` `push()`, `pop()`, `splice()`, `sort()` і `reverse()` змінюють оригінальний масив. Заміняй їх на немутуючі альтернативи. **3. Хибне уявлення що spread копіює вкладені об'єкти** ```tsx // ❌ Частково неправильно - видаляє всі інші поля address setUser({ ...user, address: { city: "NYC" } }); // ✅ Правильно - spread і на вкладеному рівні setUser({ ...user, address: { ...user.address, city: "NYC" } }); ``` Spread поверхневий. Копіює поля першого рівня, але вкладені об'єкти досі є спільними посиланнями. **4. Мутація елемента всередині масиву після spread масиву** ```tsx // ❌ Неправильно - масив новий, але todos[0] досі той самий об'єкт const next = [...todos]; next[0].done = true; setTodos(next); // ✅ Правильно - новий масив, новий об'єкт елемента setTodos(todos.map(todo => todo.id === id ? { ...todo, done: true } : todo )); ``` На практиці це найвідступніший баг мутації. Розробник, який вже знає "не мутуй масив напряму", робить spread, почувається впевнено, а потім годину дебажить чому елемент списку не оновився. Масив новий. Об'єкти всередині - ні. **5. Читання стану одразу після setState** ```tsx // ❌ Хибне очікування - count досі старе значення setCount(count + 1); console.log(count); // ✅ Функція-оновлювач, коли новий стан залежить від попереднього setCount(prev => prev + 1); ``` `setState` лише планує оновлення на наступний рендер. Змінна не оновлюється синхронно. ### Де зустрічається - `useState` - кожне оновлення потребує нового посилання для ре-рендеру - Reducers у Redux - повинні повертати новий стан, ніколи не мутувати аргумент - Zustand - функції оновлення стану мають повертати нові об'єкти - React Query - оновлення кешу дотримуються тих самих правил - Immer - дає змогу писати код у стилі мутації, який виробляє незмінні результати - `React.memo` і `useMemo` - стабільні посилання запобігають зайвим ре-рендерам дочірніх компонентів ### Питання на співбесіді **Q:** Чому React використовує порівняння посилань, а не глибоке порівняння? **A:** Глибоке порівняння перевіряє кожну вкладену властивість рекурсивно - це O(n) на кожне оновлення стану. Порівняння посилань - O(1). Крім того, це робить намір явним: створення нового об'єкта сигналізує "це змінене значення." **Q:** Що станеться, якщо я мутував стан, але компонент все одно ре-рендерується через зміну пропсів батька? **A:** Компонент ре-рендериться, але зі зламаним станом. Твоя мутація змінила старий об'єкт. Внутрішній стан React не оновлювався. Рендер використовує те, що є у fiber, ігноруючи мутацію. Отримуєш розбіжність у UI і важкий для відладки баг. **Q:** Як Immer вирішує проблему глибоко вкладених оновлень без нескінченних spread-операторів? **A:** Immer загортає стан у Proxy, який називається "draft" (чернетка). Ти пишеш пряму мутацію (`draft.user.address.city = "NYC"`), а Immer відстежує кожну зміну і виробляє новий незмінний об'єкт. Копіює лише ті гілки, які справді змінились, тому ефективний навіть для великих дерев стану. **Q:** У мене 10 000 елементів у масиві. Чи не марнотратно створювати новий масив при кожному оновленні? **A:** Spread великих масивів швидкий у сучасних JS-рушіях, тому здебільшого це не проблема. Але якщо часто оновлюєш окремі елементи, нормалізуй стан: зберігай елементи як об'єкт з ключами-ID замість масиву. Тоді оновлення одного елемента - це одне присвоєння властивості, а не копіювання всього масиву. ## Приклади ### Оновлення форми з вкладеними налаштуваннями ```tsx const [formData, setFormData] = useState({ email: "", preferences: { notifications: true, theme: "dark" } }); function handleEmailChange(e) { // Змінюється тільки email; посилання на preferences залишається тим самим setFormData({ ...formData, email: e.target.value }); } function toggleNotifications() { // Новий зовнішній об'єкт і новий об'єкт preferences setFormData({ ...formData, preferences: { ...formData.preferences, notifications: !formData.preferences.notifications } }); } ``` `handleEmailChange` створює новий об'єкт верхнього рівня. Властивість `preferences` досі вказує на той самий об'єкт - це нормально, бо вона не змінювалась. `toggleNotifications` потребує нового об'єкта `preferences`, бо саме він змінився. ### Оновлення одного елемента в масиві об'єктів ```tsx const [todos, setTodos] = useState([ { id: 1, title: "Вивчити React", tags: ["js", "react"] }, { id: 2, title: "Зробити проєкт", tags: ["project"] } ]); function addTagToTodo(todoId, newTag) { setTodos(todos.map(todo => todo.id === todoId ? { ...todo, tags: [...todo.tags, newTag] } : todo )); } ``` `map()` повертає новий масив. Потрібний todo отримує новий об'єкт з новим масивом `tags`. Інші todos зберігають оригінальні посилання. Це важливо для `React.memo` на елементах списку - незмінені елементи не будуть ре-рендеритися. ### Складні вкладені оновлення з Immer ```tsx import { produce } from "immer"; const [state, setState] = useState({ users: [ { id: 1, name: "Alice", scores: [90, 85] } ] }); function addScore(userId, score) { setState(produce(draft => { const user = draft.users.find(u => u.id === userId); if (user) { user.scores.push(score); // Immer відстежує це і виробляє новий об'єкт } })); } ``` Без Immer потрібно spread'ити стан, масив `users`, знайти користувача, spread'ити його і spread'ити масив `scores`. П'ять рівнів spread для однієї зміни. Immer робить все це внутрішньо. Намір зрозумілий.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.