DEV_NET_CORE
GET_STARTED
ReactComponents, props, state, and rendering behavior

Controlled inputs and event handling

Overview

Controlled inputs and event handling are core React skills because most real interfaces respond to user input: typing in forms, clicking buttons, selecting options, submitting data, dismissing dialogs, and changing filters. React handles these interactions declaratively. Instead of manually reading and mutating DOM fields, a component stores important input values in state and renders the UI from that state.

Code
function SearchBox() {
  const [query, setQuery] = useState("");

  return (
    <input
      value={query}
      onChange={(event) => setQuery(event.target.value)}
    />
  );
}

This is a controlled input: React state is the source of truth, and the input displays whatever value React passes to it. When the user types, onChange updates state, React re-renders, and the input receives the new value.

Event handling is the other half of the pattern. React lets you pass functions to JSX event props such as onClick, onChange, onSubmit, onKeyDown, and onBlur. These handlers run in response to user interactions and often update state, call parent callbacks, prevent default browser behavior, or coordinate UI transitions.

For interviews, this topic matters because controlled inputs reveal whether a developer understands React's one-way data flow, state updates, event handler timing, form submission, accessibility, and common pitfalls such as calling handlers during render or switching an input between controlled and uncontrolled modes.

Core Concepts

Event Handlers

An event handler is a function passed to a JSX event prop.

Code
function SaveButton() {
  function handleClick() {
    console.log("Saving...");
  }

  return <button onClick={handleClick}>Save</button>;
}

The handler is passed, not called. This is correct:

Code
<button onClick={handleClick}>Save</button>

This is wrong:

Code
<button onClick={handleClick()}>Save</button>

The second version runs handleClick during render instead of waiting for a click.

Inline handlers are fine for short logic:

Code
<button onClick={() => setOpen(true)}>Open</button>

For more complex logic, use a named handler:

Code
function handleSubmitClick() {
  validateForm();
  submitForm();
}

Handler Naming Conventions

By convention:

  • Handler functions inside a component often start with handle.
  • Handler props passed into a component often start with on.
Code
function Toolbar({
  onSave,
  onCancel,
}: {
  onSave: () => void;
  onCancel: () => void;
}) {
  return (
    <div>
      <button onClick={onSave}>Save</button>
      <button onClick={onCancel}>Cancel</button>
    </div>
  );
}

Use domain-specific names when they make intent clearer:

Code
<VideoControls
  onPlayMovie={playMovie}
  onUploadImage={uploadImage}
/>

This keeps parent-child contracts meaningful. A reusable Button might expose onClick, but a feature component should often expose onSelectUser, onSubmitOrder, or onCloseDialog.

Event Objects

React passes an event object to event handlers.

Code
function TextInput() {
  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
    console.log(event.target.value);
  }

  return <input onChange={handleChange} />;
}

Common event fields and methods:

  • event.target: the element where the event originated.
  • event.currentTarget: the element the handler is attached to.
  • event.preventDefault(): prevents default browser behavior.
  • event.stopPropagation(): stops the event from bubbling to parent handlers.

In TypeScript, currentTarget is often easier to type safely because it refers to the element that owns the handler:

Code
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
  setEmail(event.currentTarget.value);
}

Event Propagation

Most React events bubble up the component tree. If a child and parent both listen for clicks, the child handler runs first, then the parent handler.

Code
function Toolbar() {
  return (
    <div onClick={() => console.log("toolbar")}>
      <button onClick={() => console.log("button")}>
        Save
      </button>
    </div>
  );
}

Clicking the button logs both messages.

To stop the event from reaching the parent:

Code
function StopButton({ onClick }: { onClick: () => void }) {
  return (
    <button
      onClick={(event) => {
        event.stopPropagation();
        onClick();
      }}
    >
      Save
    </button>
  );
}

Use stopPropagation deliberately. Often, explicit callback chains are easier to trace than relying on bubbling.

Preventing Default Form Behavior

HTML forms submit by default, which may reload the page. React form handlers usually prevent that default behavior and handle submission in JavaScript.

Code
function LoginForm() {
  const [email, setEmail] = useState("");

  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    submitLogin(email);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={email}
        onChange={(event) => setEmail(event.currentTarget.value)}
      />
      <button type="submit">Log in</button>
    </form>
  );
}

Prefer onSubmit on the form over onClick on the submit button. It supports Enter key submission and works better with browser form semantics.

Controlled Inputs

A controlled input receives its value from React state and reports changes through an event handler.

Code
function NameField() {
  const [name, setName] = useState("");

  return (
    <input
      value={name}
      onChange={(event) => setName(event.currentTarget.value)}
    />
  );
}

The value shown in the input is always the name state value. Typing triggers onChange, the handler updates name, and React re-renders with the new value.

Controlled inputs are useful when you need to:

  • Validate as the user types.
  • Enable or disable buttons based on input.
  • Format or normalize values.
  • Reset a form from state.
  • Submit values from React state.
  • Keep multiple fields or components synchronized.

Controlled Textareas

In React, a controlled <textarea> uses value and onChange, not children text.

Code
function CommentBox() {
  const [comment, setComment] = useState("");

  return (
    <textarea
      value={comment}
      onChange={(event) => setComment(event.currentTarget.value)}
    />
  );
}

Use defaultValue for an uncontrolled initial value:

Code
<textarea defaultValue="Initial comment" />

Do not mix value and defaultValue for the same field.

Controlled Selects

A controlled <select> also uses value and onChange.

Code
function RoleSelect() {
  const [role, setRole] = useState("user");

  return (
    <select
      value={role}
      onChange={(event) => setRole(event.currentTarget.value)}
    >
      <option value="admin">Admin</option>
      <option value="user">User</option>
      <option value="guest">Guest</option>
    </select>
  );
}

For multiple select, the selected value is usually modeled as an array:

Code
function TagSelect() {
  const [tags, setTags] = useState<string[]>([]);

  return (
    <select
      multiple
      value={tags}
      onChange={(event) => {
        const selected = Array.from(
          event.currentTarget.selectedOptions,
          (option) => option.value
        );

        setTags(selected);
      }}
    >
      <option value="react">React</option>
      <option value="typescript">TypeScript</option>
      <option value="testing">Testing</option>
    </select>
  );
}

Checkboxes and Radio Buttons

Text inputs use value; checkboxes and radio buttons use checked.

Code
function NewsletterCheckbox() {
  const [subscribed, setSubscribed] = useState(false);

  return (
    <label>
      <input
        type="checkbox"
        checked={subscribed}
        onChange={(event) => setSubscribed(event.currentTarget.checked)}
      />
      Subscribe
    </label>
  );
}

Radio buttons commonly share one state value:

Code
function PlanPicker() {
  const [plan, setPlan] = useState("basic");

  return (
    <fieldset>
      <label>
        <input
          type="radio"
          name="plan"
          value="basic"
          checked={plan === "basic"}
          onChange={(event) => setPlan(event.currentTarget.value)}
        />
        Basic
      </label>
      <label>
        <input
          type="radio"
          name="plan"
          value="pro"
          checked={plan === "pro"}
          onChange={(event) => setPlan(event.currentTarget.value)}
        />
        Pro
      </label>
    </fieldset>
  );
}

Common checkbox mistake:

Code
setSubscribed(event.currentTarget.value);

Use checked, not value, for booleans.

Controlled vs Uncontrolled Inputs

A controlled input is driven by React state:

Code
<input value={name} onChange={(event) => setName(event.currentTarget.value)} />

An uncontrolled input lets the DOM manage the current value:

Code
<input defaultValue="Ava" />

Controlled inputs are best when React needs to know and control the value. Uncontrolled inputs are fine for simple forms, integration with non-React code, or values read only on submit through FormData or refs.

Do not switch a field between controlled and uncontrolled during its lifetime:

Code
<input value={maybeName} onChange={handleChange} />

If maybeName starts as undefined and later becomes a string, React treats that as switching modes. Use a stable fallback:

Code
<input value={maybeName ?? ""} onChange={handleChange} />

Synchronous Updates for Controlled Inputs

A controlled input should synchronously update its backing state in onChange.

Code
function NameField() {
  const [name, setName] = useState("");

  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
    setName(event.currentTarget.value);
  }

  return <input value={name} onChange={handleChange} />;
}

Avoid delaying the state update that controls the input:

Code
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
  setTimeout(() => {
    setName(event.currentTarget.value);
  }, 100);
}

This can make the input feel broken because React keeps rendering the old value while the user types. If expensive work is needed, update the input value immediately and defer the expensive derived work separately.

Form State Shape

For small forms, separate state variables are readable:

Code
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");

For larger forms, object state can reduce repetition:

Code
type LoginForm = {
  email: string;
  password: string;
};

function LoginForm() {
  const [form, setForm] = useState<LoginForm>({
    email: "",
    password: "",
  });

  function updateField<K extends keyof LoginForm>(key: K, value: LoginForm[K]) {
    setForm((current) => ({
      ...current,
      [key]: value,
    }));
  }

  return (
    <form>
      <input
        value={form.email}
        onChange={(event) => updateField("email", event.currentTarget.value)}
      />
      <input
        type="password"
        value={form.password}
        onChange={(event) => updateField("password", event.currentTarget.value)}
      />
    </form>
  );
}

When using object state, remember that setting state replaces the object. Copy unchanged fields with spread.

Declarative Form UI

React encourages describing the form's visual states, then deriving the UI from state.

Code
type Status = "idle" | "submitting" | "success" | "error";

function ContactForm() {
  const [status, setStatus] = useState<Status>("idle");
  const [message, setMessage] = useState("");

  const isSubmitting = status === "submitting";

  async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setStatus("submitting");

    try {
      await sendMessage(message);
      setStatus("success");
    } catch {
      setStatus("error");
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <textarea
        value={message}
        disabled={isSubmitting}
        onChange={(event) => setMessage(event.currentTarget.value)}
      />
      <button disabled={isSubmitting || message.trim() === ""}>
        Send
      </button>
      {status === "error" && <p role="alert">Failed to send.</p>}
    </form>
  );
}

The UI follows state. The code does not manually enable, disable, show, or hide DOM nodes imperatively.

Accessibility and Semantic Events

Use the right HTML element for the interaction.

Good:

Code
<button onClick={onClose}>Close</button>

Risky:

Code
<div onClick={onClose}>Close</div>

A real button supports keyboard interaction, focus behavior, disabled state, and semantic meaning. If a clickable element is truly a button, use <button>.

For forms:

  • Use <label htmlFor="fieldId"> or wrap the input in a label.
  • Use type="submit" for submit buttons.
  • Use type="button" for non-submit buttons inside forms.
  • Use aria-invalid, aria-describedby, and role="alert" where appropriate for validation errors.

Common Mistakes

Common mistakes include:

  • Calling a handler during render: onClick={handleClick()}.
  • Passing value without onChange for an editable field.
  • Using value instead of checked for checkboxes.
  • Switching an input from uncontrolled to controlled by using undefined or null.
  • Using defaultValue and expecting later state changes to update the field.
  • Handling form submission only on a button click instead of onSubmit.
  • Forgetting event.preventDefault() for JavaScript form submission.
  • Overusing stopPropagation instead of designing explicit callbacks.
  • Using <div onClick> when a semantic <button> is appropriate.
  • Doing expensive validation synchronously on every keystroke without considering responsiveness.

Best Practices

Use these rules of thumb:

  • Use controlled inputs when React needs to validate, submit, reset, or coordinate field values.
  • Use value for text-like fields and checked for checkbox/radio booleans.
  • Update controlled input state synchronously in onChange.
  • Use onSubmit on forms and call preventDefault for JavaScript submission.
  • Pass event handlers as functions, not function calls.
  • Name feature-level callback props after user intent.
  • Keep side effects in event handlers, not render logic.
  • Prefer semantic HTML elements for accessibility.
  • Keep form state as simple as possible.

Interview Practice

PreviousUtility types and conditional typesNext UpFunctional components and JSX composition