skip to content

In React Native, how do a TextInput's onChangeText and onChange callbacks differ, and when would you pass defaultValue instead of value?

level: juniorimportance: must knowfreq 70%

answer

  1. the native field owns the text
  2. one callback gets a string
  3. the other gets nativeEvent
  4. eventCount rides along
  5. defaultValue: initial text only

basics

~20 s

onChangeText receives the new text as a plain string; onChange receives the native event, whose nativeEvent carries text, eventCount and target. Both fire for the same edit. defaultValue seeds an uncontrolled field; value makes the native field match your state.

solid answer

~40 s

Every edit in a React Native `TextInput` produces one native change event. `TextInput` calls `onChange` with the full event first, whose `nativeEvent` holds `text`, `eventCount` and `target`, and then `onChangeText` with just the new string. Most code only needs `onChangeText`; `onChange` is for when you need the event itself. `value` makes the field controlled: after each render React Native forces the native text to match it. `defaultValue` only provides the initial text; from then on the native field owns the text and you read it when you need it, for example from `nativeEvent.text` in `onEndEditing` or `onSubmitEditing`. Uncontrolled fields avoid a render per keystroke, which suits long forms whose values you only need on submit.

code

tsx · 29 lines
tsx
import {useState} from 'react';
import {TextInput} from 'react-native';
import type {TextInputChangeEvent} from 'react-native';

export function StreetField({saved}: {saved: string}) {
  const [street, setStreet] = useState(saved);

  return (
    <TextInput
      value={street}
      onChangeText={setStreet}
      onChange={(e: TextInputChangeEvent) =>
        console.log('edit', e.nativeEvent.eventCount)
      }
      placeholder="Street address"
    />
  );
}

export function UnitField({saved, onDone}: {saved: string; onDone: (t: string) => void}) {
  // Uncontrolled: the native field owns the text; read it when editing ends.
  return (
    <TextInput
      defaultValue={saved}
      onEndEditing={e => onDone(e.nativeEvent.text)}
      placeholder="Apartment or unit"
    />
  );
}

go deeper

for a junior

Recall that onChangeText gives a string and onChange gives the native event, and that value controls the field while defaultValue only seeds it.

for a middle

Explain that the native field owns the text, the order of the two callbacks, and how value is pushed back to the native view after a render.

for a senior

Choose controlled or uncontrolled per form based on validation needs and render cost, and read uncontrolled values through onEndEditing, onSubmitEditing or a ref.

for a principal

Set a form convention for the app, weighing live feedback against per-keystroke renders on low-end devices and consistency across teams.

## Where the text actually lives A React Native `TextInput` is a real platform text field: a `UITextField` or `UITextView` on iOS, an `EditText` on Android. The **native field owns the characters** the user sees. When the user types, the platform updates the field immediately and React Native then tells JavaScript what happened. That ordering explains most of the API. ## The two change callbacks Each edit produces one native change event, and `TextInput` fans it out to two props, in this order: 1. **`onChange(event)`** receives the event object. Its `nativeEvent` contains: - `text`, the full new text; - `eventCount`, a counter the native field increments on every change; - `target`, the native node that fired. 2. **`onChangeText(text)`** receives only the new text as a string. | | `onChangeText` | `onChange` | |---|---|---| | Argument | `string` | event with `nativeEvent: {text, eventCount, target}` | | Typical use | updating state for a field | reading the event, or a shared handler that needs `target` | | Fires | on every edit | on every edit, just before `onChangeText` | `onChangeText` is the idiomatic choice. A generic handler that needs more than the text uses `onChange`. You rarely need both on one field. ## value: controlled Passing `value` makes the field **controlled**. After each render, if `value` differs from the last text the native field reported, React Native sends the new text down to the native view. The `eventCount` from the change event is part of that exchange: the native side ignores an update from JavaScript that was computed from an older event than the one it has already applied, so a slow render cannot overwrite fresher typing. Controlled is right when the UI must react to every keystroke: - live validation or a character counter; - enabling a button as soon as the field is non-empty; - deriving other state from the text as it changes. The cost is a JavaScript render per keystroke, and the forced sync can flicker if you rewrite or reject what the user typed. ## defaultValue: uncontrolled `defaultValue` gives the field its **initial text** and nothing more. The native field then owns the text and nothing is forced back. You read the value when you need it: - `onEndEditing` and `onSubmitEditing` both receive `nativeEvent.text`; - `onChangeText` can still record the latest text in a ref without causing renders; - `ref.current?.clear()` empties the field without touching any state. The docs warn that `onBlur`'s `nativeEvent.text` may be `undefined`; use `onEndEditing` to get the final text instead. ## Common mistakes - **Reading `event.target.value`.** That is a browser habit; in React Native the text is the string passed to `onChangeText`, or `event.nativeEvent.text` in `onChange`. - **Passing `value` without a change handler.** The native field still accepts the keystroke, then React Native forces it back to the unchanged `value`, so the field looks broken: characters appear and vanish. Either handle `onChangeText` or make the field non-editable with `editable={false}`. - **Doing heavy work in `onChangeText`.** It runs on every keystroke on the JavaScript thread; expensive validation or a render of the whole screen per character makes typing feel sluggish. - **Expecting a diff.** `onChangeText` always receives the complete current text, not the characters just typed. ## Choosing in a checkout form A grocery checkout collects a street address, an apartment or unit, a postcode and a phone number for the courier. Two sensible designs: - **Controlled fields** if the form validates as the user types and enables **Place order** live. Four small fields re-rendering per keystroke is cheap. - **Uncontrolled fields** with `defaultValue` prefilled from the saved address, reading each value on submit, if the form only validates at the end. This avoids re-rendering the whole form on each keystroke, which matters on a slow device or a large form. Mixing both on one field is a bug: pass `value` or `defaultValue`, not both, and do not switch a field between them during its life.

  • In React Native, how do you read the final text of an uncontrolled TextInput without tracking every keystroke in state?
    Read it from the events that carry it: `onEndEditing` and `onSubmitEditing` both pass `nativeEvent.text`. Alternatively keep the latest string from `onChangeText` in a ref, which does not trigger renders. Avoid relying on `onBlur`'s `nativeEvent.text`, which the docs warn can be `undefined`.
  • In React Native, what is eventCount in a TextInput's onChange event used for?
    It is a counter the native field increments on every change. `TextInput` remembers the latest count and sends it along when it pushes a controlled `value` back to the native view. The native side ignores an update whose count is not its current one, so a render based on an older keystroke cannot overwrite text the user has typed since.

saying these in an interview costs you the question

  • onChangeText receives an event object and you read event.target.value from it.
  • onChange and onChangeText fire for different kinds of edits, so a field needs both.
  • defaultValue keeps the field in sync whenever the prop changes later.
  • Without value the text is lost, so an uncontrolled TextInput cannot be read on submit.
  • A controlled TextInput renders the keystroke only after JavaScript sets state.