Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Контрольовані та неконтрольовані компоненти в React». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Контрольований компонент** зберігає значення форми в стані React і ре-рендериться при кожному введенні. Неконтрольований зберігає значення в DOM і ти зчитуєш його через ref за потреби. ```jsx // Контрольований: value + onChange завжди разом <input value={email} onChange={(e) => setEmail(e.target.value)} /> // Неконтрольований: ref + defaultValue const ref = useRef(); <input ref={ref} defaultValue="" /> // ref.current.value при відправці ``` **Ключове:** контрольований для валідації в реальному часі; неконтрольований для простих форм, file-інпутів або сторонніх бібліотек.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Контрольований компонент** зберігає значення форми в стані React і синхронізує його при кожному введенні. **Неконтрольований компонент** дозволяє DOM тримати значення, а ти зчитуєш його через ref тоді, коли потрібно. ## Теорія ### TL;DR - Контрольований: стан React є єдиним джерелом правди, `onChange` спрацьовує при кожному натисканні і запускає ре-рендер - Неконтрольований: DOM тримає значення, ти зчитуєш його через `ref` за потреби - Аналогія: контрольований - це живий чат, який синхронізує кожен символ; неконтрольований - паперова форма, яку заповнюєш і здаєш в кінці - Використовуй контрольований для валідації в реальному часі або умовного рендерингу - Використовуй неконтрольований для простих форм, file-інпутів або інтеграції зі сторонніми бібліотеками ### Швидкий приклад ```jsx // КОНТРОЛЬОВАНИЙ: React тримає значення function Controlled() { const [name, setName] = useState(""); return ( <input value={name} onChange={(e) => setName(e.target.value)} /> // name завжди синхронізовано з тим, що ввів користувач ); } // НЕКОНТРОЛЬОВАНИЙ: DOM тримає значення function Uncontrolled() { const inputRef = useRef(); return ( <> <input ref={inputRef} /> <button onClick={() => console.log(inputRef.current.value)}> Відправити </button> {/* значення зчитується тільки при кліку */} </> ); } ``` У контрольованому `value` і `onChange` завжди йдуть разом. У неконтрольованому `ref` вказує безпосередньо на DOM-вузол. ### Ключова різниця У контрольованому компоненті стан React є єдиним джерелом правди. Кожне натискання клавіші запускає `onChange`, оновлює стан і викликає ре-рендер з новим значенням пропу `value`. У неконтрольованому браузер оновлює DOM напряму, нічого не повідомляючи React. Коли ти читаєш `inputRef.current.value`, ти повністю обходиш React і звертаєшся до DOM-вузла. Тобто контрольований інпут завжди синхронізований зі станом React, а неконтрольований може тримати значення, про яке React нічого не знає. ### Коли що використовувати **Контрольований:** - Валідація в реальному часі - кнопка submit неактивна поки email некоректний - Умовний рендеринг залежно від введення - підказки автодоповнення при наборі - Форми з попередньо заповненими даними з API або пропів - Будь-яка ситуація, де React має реагувати на кожну зміну **Неконтрольований:** - Прості форми, де значення потрібне тільки при відправці - File-інпути (`<input type="file">`) - браузери блокують програмне встановлення `value` з міркувань безпеки - Інтеграція зі сторонніми DOM-бібліотеками, які самі керують своїм станом - Форми з великою кількістю полів, де ре-рендер на кожне натискання викликає помітні затримки ### Таблиця порівняння | Аспект | Контрольований | Неконтрольований | |--------|---------------|------------------| | Де зберігається значення | Стан React | DOM-елемент | | Коли синхронізується | При кожному натисканні | За запитом (через ref) | | Ре-рендер | Так, при кожній зміні | Ні, якщо не запустиш вручну | | Валідація | Доступна в реальному часі | Тільки при відправці | | Проп для початкового значення | `value` | `defaultValue` | | Коли використовувати | Валідація, умовний UI, попередньо заповнені поля | Прості форми, file-інпути, сторонні бібліотеки | ### Як це працює зсередини Коли ти вводиш текст у контрольований інпут, браузер генерує подію `onChange`. Обробник React викликає сетер [`useState`](/questions/usestate), планує ре-рендер, порівнює старий і новий virtual DOM і оновлює реальний DOM з новим пропом `value`. Все це відбувається в рамках одного циклу події. Для неконтрольованих інпутів браузер оновлює DOM напряму. Virtual DOM React цю зміну взагалі не бачить. Читання [`inputRef.current.value`](/questions/useref) йде прямо до DOM-вузла, повністю минаючи React. Ні ре-рендеру, ні оновлення стану, ні порівняння. ### Типові помилки **1. Використання `value` без `onChange`** ```jsx // НЕПРАВИЛЬНО: інпут стає read-only <input value={name} /> // ПРАВИЛЬНО: завжди йдуть разом <input value={name} onChange={(e) => setName(e.target.value)} /> ``` React встановлює значення пропу, але не має механізму для його оновлення. Користувач не може нічого ввести. React також виводить попередження в консоль. **2. Використання `value` замість `defaultValue` у неконтрольованих компонентах** ```jsx // НЕПРАВИЛЬНО: це робить інпут контрольованим без обробника onChange <input ref={inputRef} value="initial" /> // ПРАВИЛЬНО: defaultValue встановлює початкове значення, не беручи контроль <input ref={inputRef} defaultValue="initial" /> ``` **3. Зчитування ref до монтування компонента** ```jsx // НЕПРАВИЛЬНО: inputRef.current є null під час першого рендеру function Form() { const inputRef = useRef(); const value = inputRef.current.value; // TypeError: null return <input ref={inputRef} />; } // ПРАВИЛЬНО: читай ref в обробниках подій або ефектах function Form() { const inputRef = useRef(); const handleClick = () => { console.log(inputRef.current.value); // тут безпечно }; return <input ref={inputRef} />; } ``` **4. Ініціалізація стану контрольованого компонента як `undefined`** ```jsx // НЕПРАВИЛЬНО: undefined означає відсутність пропу value, React вважає інпут неконтрольованим const [name, setName] = useState(); <input value={name} onChange={(e) => setName(e.target.value)} /> // ПРАВИЛЬНО: ініціалізуй порожнім рядком const [name, setName] = useState(""); <input value={name} onChange={(e) => setName(e.target.value)} /> ``` React виводить попередження "A component is changing an uncontrolled input to be controlled." Це трапляється тому, що `undefined` дорівнює відсутності пропу `value` на першому рендері. **5. Запуск важких операцій при кожному натисканні** ```jsx // ПОВІЛЬНО: expensiveSearch запускається при кожному символі const handleChange = (e) => { setSearch(e.target.value); expensiveSearch(e.target.value); }; // КРАЩЕ: debounce для важкої частини, стан оновлюється одразу const handleSearch = useCallback( debounce((value) => expensiveSearch(value), 300), [] ); const handleChange = (e) => { setSearch(e.target.value); handleSearch(e.target.value); }; ``` ### Де це зустрічається - React Hook Form за замовчуванням використовує неконтрольовані інпути - зберігає значення в ref замість стану, щоб уникнути ре-рендерингу при кожному натисканні - Material-UI і Chakra UI передають `value` і `onChange` до всіх компонентів форм - контрольований патерн за замовчуванням - `<input type="file">` завжди неконтрольований, бо браузери блокують програмне встановлення `value` з міркувань безпеки - Redux-форми зберігають значення полів у Redux-стані - це контрольований патерн на глобальному рівні - Server Actions у Next.js добре поєднуються з неконтрольованими формами через `FormData` API, без будь-якого стану ### Питання на співбесіді **Q:** Чому React попереджає "You provided a `value` prop without an `onChange` handler"? **A:** Бо інпут стає read-only. React встановлює значення, але не має механізму для його оновлення, тому користувач нічого не може ввести. React виявляє цю невідповідність завчасно, щоб ти не витрачав час на дебагінг замороженого поля. **Q:** Чи може компонент перемкнутись з неконтрольованого на контрольований під час роботи? **A:** Ні. React кидає помилку при переключенні в будь-який бік. Потрібно обрати один патерн і дотримуватись його протягом усього часу існування компонента. **Q:** У чому реальна різниця у продуктивності? **A:** Контрольовані компоненти ре-рендеряться при кожному натисканні. З великими формами або важкою логікою рендеру це стає помітним. Неконтрольовані повністю пропускають цикл рендерингу React, але ти втрачаєш валідацію в реальному часі. React Hook Form вирішує це, використовуючи неконтрольовані інпути внутрішньо з контрольованим API назовні. **Q:** Форма з 50 полями - який підхід обрати? **A:** Контрольований з оптимізацією. Розбий форму на менші підкомпоненти, мемоїзуй обробники через `useCallback` і додай debounce для важких операцій. Або використовуй React Hook Form, який вирішує це внутрішньо. **Q:** (Senior) Чому React Hook Form за замовчуванням використовує неконтрольовані компоненти і коли це варто змінити? **A:** React Hook Form зберігає значення в ref, а не в стані, щоб уникнути ре-рендерингу форми при кожному введенні. Ти переходиш до контрольованого режиму (через `Controller` або `useController`), коли інтегруєш контрольовані UI-бібліотеки типу Material-UI або коли потрібен умовний рендеринг на основі значення інпуту в реальному часі. ## Приклади ### Контрольована форма з валідацією email ```jsx function SignupForm() { const [email, setEmail] = useState(""); const [error, setError] = useState(""); const handleChange = (e) => { const value = e.target.value; setEmail(value); if (value && !value.includes("@")) { setError("Некоректний email"); } else { setError(""); } }; return ( <div> <input value={email} onChange={handleChange} placeholder="Email" /> {error && <span style={{ color: "red" }}>{error}</span>} {/* повідомлення з'являється і зникає при наборі */} </div> ); } ``` Повідомлення про помилку реагує в реальному часі, бо контрольований інпут ре-рендериться при кожному натисканні. З неконтрольованим ти б побачив некоректний email тільки після кліку на submit. ### Неконтрольована форма для простої відправки ```jsx function ContactForm() { const nameRef = useRef(); const emailRef = useRef(); const handleSubmit = (e) => { e.preventDefault(); const data = { name: nameRef.current.value, email: emailRef.current.value, }; console.log(data); // зчитуємо обидва значення одразу при відправці }; return ( <form onSubmit={handleSubmit}> <input ref={nameRef} defaultValue="" placeholder="Ім'я" /> <input ref={emailRef} defaultValue="" placeholder="Email" /> <button type="submit">Відправити</button> </form> ); } ``` При наборі ре-рендерів немає. React включається тільки при відправці. Добре підходить для простих форм без потреби в реальному зворотному зв'язку. ### Чому не варто змішувати обидва підходи ```jsx // НЕ РОБИ ТАК function BadMix() { const [value, setValue] = useState(""); const inputRef = useRef(); return ( <> <input value={value} onChange={(e) => setValue(e.target.value)} ref={inputRef} /> <button onClick={() => console.log(inputRef.current.value)}> Вивести значення </button> {/* ref читає DOM-вузол коректно, але стан вже тримає те саме */} {/* два способи отримати одні дані - обери один */} </> ); } ``` Ref зчитує DOM-вузол правильно. Але стан вже тримає те саме значення. Використовувати обидва - це зайвий когнітивний overhead і пряме питання: "Якому з двох джерел вірити?". Такий патерн нерідко з'являється в код-рев'ю, коли розробники приходять з jQuery і намагаються поєднати обидва світи. Обери один підхід і дотримуйся його.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.