Controlled (chuẩn React, recommend default): state cha giữ value, onChangeText cập nhật.
const [name, setName] = useState('')
<TextInput value={name} onChangeText={setName} />Lợi thế: validate/format on-the-fly, sync với store toàn cục, easy reset.
Uncontrolled: dùng defaultValue và onChangeText cache giá trị vào ref ngoài, không bind vào React state.
const valueRef = useRef('')
<TextInput
defaultValue=""
onChangeText={(t) => { valueRef.current = t }}
/>
// Lúc submit:
console.log(valueRef.current)RN không có inputRef.current.value như DOM web — phải tự cache qua onChangeText.
Thực tế RN, uncontrolled thuần ít gặp; pattern thường gặp hơn là react-hook-form dùng Controller ẩn ref bên trong → có cảm giác controlled cho user nhưng không re-render parent mỗi keystroke. Form lớn (10+ inputs) trên Android low-end thấy khác biệt rõ.
Ngoài ra, controlled <TextInput> từng có vấn đề race condition trên Android dưới Bridge cũ: user gõ 5 ký tự liên tục, JS state update async qua bridge, đôi khi caret nhảy ngược hoặc ký tự "rớt". New Architecture với JSI làm commit đồng bộ → vấn đề này biến mất ở RN 0.76+.
Controlled (standard React, recommended default): the parent state holds the value, onChangeText updates it.
const [name, setName] = useState('')
<TextInput value={name} onChangeText={setName} />Upsides: live validation/formatting, sync to a global store, easy reset.
Uncontrolled: use defaultValue and cache values to an external ref via onChangeText, not React state.
const valueRef = useRef('')
<TextInput
defaultValue=""
onChangeText={(t) => { valueRef.current = t }}
/>
// On submit:
console.log(valueRef.current)RN has no inputRef.current.value like the DOM — you must cache it yourself via onChangeText.
In practice, pure uncontrolled is rare; the common pattern is react-hook-form using Controller, which hides the ref internally — feels controlled to the developer but does not re-render the parent on every keystroke. Large forms (10+ inputs) on low-end Android show the difference clearly.
Also, controlled <TextInput> historically had a race condition on Android under the legacy Bridge: rapid typing with async JS state updates over the bridge sometimes made the caret jump or characters drop. The New Architecture with JSI makes commits synchronous, so this issue disappears in RN 0.76+.