React Forms and Events Interview Questions

Controlled versus uncontrolled inputs, synthetic events, event delegation, validation, and the React 19 form actions that replace most of the boilerplate.

11 questions4 fresher, 5 mid-level, 2 senior

Forms are the most common practical exercise in a React interview, because a form touches state, events, validation, accessibility and performance in one small problem. Expect "build a login form" or "add validation to this" as a live coding task, and expect the controlled versus uncontrolled question to come up whether or not you raise it.

1.What is a controlled component?

Fresher

An input whose displayed value comes from React state and whose changes are written back to that state through an onChange handler. React is the single source of truth for the field.

function NameField() {
  const [name, setName] = useState('');
  return (
    <input
      value={name}
      onChange={(event) => setName(event.target.value)}
    />
  );
}
  • You can transform input as it is typed: force uppercase, strip characters, format a card number.
  • You can disable or enable other controls based on the current value, because you always have it.
  • The cost is a re-render per keystroke, which only matters when the component being re-rendered is large.

Likely follow-up questions

  • What warning do you get if you switch from uncontrolled to controlled?
  • Is a controlled input slow?

2.What is an uncontrolled component, and when would you prefer one?

Fresher

An input that keeps its own value in the DOM, which you read with a ref or from the form data on submit. Prefer it for large forms where per-keystroke re-renders hurt, for file inputs, and when integrating with non-React code.

function LoginForm({ onSubmit }) {
  function handleSubmit(event) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    onSubmit({ email: data.get('email'), password: data.get('password') });
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name='email' type='email' defaultValue='' />
      <input name='password' type='password' />
      <button>Sign in</button>
    </form>
  );
}

<input type="file"> is always uncontrolled: its value is read-only for security reasons, so you must use a ref or the form data. React Hook Form is built on this approach, which is why it re-renders so little.

Likely follow-up questions

  • Why must a file input be uncontrolled?
  • How does React Hook Form avoid re-renders?

3.What are synthetic events in React?

Mid-level

A SyntheticEvent is React's cross-browser wrapper around the native event. It has the same interface — preventDefault, stopPropagation, target — but behaves identically across browsers.

  • The native event is always available as event.nativeEvent when you need something React does not wrap.
  • Event names are camelCase and take functions, not strings: onClick={handleClick}, not onclick="handleClick()".
  • Returning false from a handler does nothing. Call event.preventDefault() explicitly.
  • React 17 removed event pooling, so you no longer need event.persist() to read an event asynchronously.
function handleSubmit(event) {
  event.preventDefault();
  console.log(event.nativeEvent); // the real DOM event
}

Likely follow-up questions

  • What was event pooling and why was it removed?
  • How do you access the native event?

4.How does React attach event handlers under the hood?

Senior

React does not attach a listener per element. It uses delegation: one listener per event type, attached to the root container the app is rendered into since React 17, and dispatches to the right component using the fibre tree.

Before React 17 the listeners were attached to document, which caused problems for apps with more than one React root and for React embedded in a page alongside other code. Moving them to the root container isolated each app.

  • Delegation keeps memory flat: a list of ten thousand rows with an onClick each still has one real listener.
  • event.stopPropagation() on a native listener attached outside React can prevent React's handler from ever running, which is a classic bug when mixing React with a third party widget.
  • Some events, such as scroll, do not bubble and are attached directly.

Likely follow-up questions

  • What changed about events in React 17?
  • Why does my native listener fire before my React handler?

5.How do you handle several inputs with a single change handler?

Fresher

Give each input a name attribute and use it as the key when updating a state object: setValues((previous) => ({ ...previous, [event.target.name]: event.target.value })).

const [values, setValues] = useState({ email: '', password: '' });

function handleChange(event) {
  const { name, value, type, checked } = event.target;
  setValues((previous) => ({
    ...previous,
    [name]: type === 'checkbox' ? checked : value,
  }));
}

<input name='email' value={values.email} onChange={handleChange} />

Likely follow-up questions

  • How would you handle a checkbox group?

6.How do you pass an argument to an event handler?

Fresher

Wrap the call in an arrow function, or bind the value with a data attribute and read it from the event. Do not call the function directly in JSX — onClick={remove(id)} runs it during render.

// Wrong: runs immediately on every render
<button onClick={remove(item.id)}>Delete</button>

// Arrow wrapper
<button onClick={() => remove(item.id)}>Delete</button>

// Data attribute, keeps one stable handler for a whole list
<button data-id={item.id} onClick={handleDelete}>Delete</button>
// const handleDelete = (e) => remove(e.currentTarget.dataset.id);

Likely follow-up questions

  • Which of those is best for a list of a thousand rows?

7.How would you validate a form in React?

Mid-level

Validate on submit and on blur rather than on every keystroke, keep errors in state keyed by field, and put the rules in a schema so the same rules can run on the server. In practice, use React Hook Form with Zod rather than writing it by hand.

const schema = z.object({
  email: z.string().email('Enter a valid email address'),
  password: z.string().min(8, 'At least 8 characters'),
});

const { register, handleSubmit, formState: { errors } } = useForm({
  resolver: zodResolver(schema),
});
  • Validating on every keystroke shows an error before the user has finished typing, which reads as hostile. Validate on blur, then on every change once a field has already errored.
  • Use the browser's own constraints too: type="email", required, min. They are free and they work before JavaScript loads.
  • Never trust client validation. It is a user experience feature; the server has to validate as well.
  • Announce errors accessibly: link the message to the input with aria-describedby and set aria-invalid.

Likely follow-up questions

  • Why validate on blur rather than on change?
  • How do you show server side validation errors?

8.Why does React's onChange behave differently from the DOM's change event?

Mid-level

React's onChange fires on every keystroke, like the native input event. The native change event only fires when the field loses focus. React deliberately normalised this so onChange means "the value changed".

If you genuinely want the native behaviour, use onBlur, which is what most validation should use anyway. This trips people up when they port DOM code into React and find their handler firing far more often than expected.

Likely follow-up questions

  • Which event would you use to validate on leaving a field?

9.What are React 19 form actions, and what do they replace?

Senior

A form can take a function as its action prop. React calls it with the FormData, tracks the pending state, resets the form on success, and integrates with useActionState, useFormStatus and useOptimistic — replacing most hand-written submit boilerplate.

function Subscribe() {
  const [state, formAction, isPending] = useActionState(
    async (previous, formData) => {
      const error = await subscribe(formData.get('email'));
      return error ? { error } : { done: true };
    },
    {}
  );

  return (
    <form action={formAction}>
      <input name='email' type='email' />
      <button disabled={isPending}>Subscribe</button>
      {state.error && <p role='alert'>{state.error}</p>}
    </form>
  );
}
  • The isSubmitting state, the try/catch, and the manual reset all disappear.
  • useFormStatus lets a nested submit button read the parent form's pending state without a prop.
  • The same action function can be a Server Action in a framework that supports them, so the form works before the JavaScript has loaded.

Likely follow-up questions

  • What is a Server Action?
  • How does progressive enhancement work here?

10.What accessibility mistakes are common in React forms?

Mid-level

Missing label associations, using a div with onClick instead of a button, error messages that are not linked to their field or announced, and losing focus after a dynamic change.

  • Every input needs a <label htmlFor> pointing at its id. Use useId to generate the id so a reusable field component works anywhere.
  • Set aria-invalid on the field and link the message with aria-describedby, and give the error container role="alert" so it is announced.
  • Use real <button> and <a> elements. A clickable div is not focusable and does not respond to Enter or Space.
  • After submitting with errors, move focus to the first invalid field or to a summary, otherwise a keyboard user has no idea what happened.
  • Do not remove focus outlines. Style them instead.

Likely follow-up questions

  • How would you test this with a screen reader?
  • What does useId solve?
Practise this in the quiz

11.What is the difference between event.target and event.currentTarget?

Mid-level

target is the element the event originated on; currentTarget is the element whose handler is currently running. In a delegated click on a list, target may be the icon inside the button while currentTarget is the button.

<ul onClick={(event) => {
  console.log(event.target);        // maybe the <span> inside the <li>
  console.log(event.currentTarget); // always the <ul>
}}>

Use currentTarget when reading a data- attribute you attached to the handler's own element, and target when you genuinely care what was clicked. Reaching for target and then walking up with closest() is a sign the handler is on the wrong element.

Likely follow-up questions

  • Which one would you use to read a data attribute?

All React interview questions