Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Forms & Controlled Components
Forms are fundamental to web applications. React provides two approaches: Controlled Components (React manages state) and Uncontrolled Components (DOM manages state). Real-world analogy: A controlled component is like a puppet on strings — React pulls every string. The input displays exactly what React state says, and every keystroke feeds back throughonChange to update that state. An uncontrolled component is like a self-service kiosk — the DOM handles the value internally, and you only read it when you need it (on submit). Most of the time, you want the puppet approach because it gives you full control over validation, formatting, and conditional logic on every keystroke.
Controlled vs Uncontrolled
| Aspect | Controlled | Uncontrolled |
|---|---|---|
| State location | React state | DOM |
| Access to value | useState variable | useRef |
| Validation | On every keystroke | On submit |
| Use case | Most forms | File inputs, quick prototypes |
Controlled Components
With controlled components, React state is the “single source of truth” for input values.Basic Text Input
Complete Form Example
Form Elements Reference
Textarea
In React,<textarea> uses a value prop instead of children:
Select
Usevalue on the <select> element, not selected on options:
Multiple Select
Radio Buttons
Form Validation
Real-Time Validation
Password Strength Indicator
Uncontrolled Components with useRef
For simple forms or when you need direct DOM access:File inputs are always uncontrolled in React because their value can only be set by the user, not programmatically.
Form Libraries
For complex forms, consider using a form library:React Hook Form
Form Pitfalls
Advanced Patterns
Debounced Input
Form with Dynamic Fields
🎯 Practice Exercises
Exercise 1: Credit Card Form
Exercise 1: Credit Card Form
Exercise 2: Multi-Step Form
Exercise 2: Multi-Step Form
Summary
| Concept | Description |
|---|---|
| Controlled Components | React state manages form input values |
| Uncontrolled Components | DOM manages values, accessed via refs |
| value + onChange | Required pattern for controlled inputs |
| event.target | Access input name, value, type, checked |
| Real-time Validation | Validate on change or blur |
| touched state | Track if user has interacted with field |
| useRef | Access DOM elements directly |
| React Hook Form | Popular library for complex forms |
| Debouncing | Delay processing for search inputs |
Next Steps
In the next chapter, you’ll learn about useEffect & Side Effects — fetching data, subscriptions, and more!
Interview Deep-Dive
Controlled vs uncontrolled components: when would you deliberately choose uncontrolled, and what are the real-world performance implications of controlled inputs?
Controlled vs uncontrolled components: when would you deliberately choose uncontrolled, and what are the real-world performance implications of controlled inputs?
Strong Answer:
I default to controlled components for nearly all form inputs because React state is the single source of truth, enabling real-time validation, conditional formatting, dependent field logic, and programmatic value changes. But there are legitimate cases for uncontrolled.File inputs must be uncontrolled — you cannot programmatically set a file input’s value in the browser for security reasons. Rich text editors like Draft.js or Slate manage their own internal state and only expose values through refs or callbacks — wrapping them in controlled state would double the state management and cause input lag.Performance-wise, a controlled input triggers a React re-render on every keystroke. For a simple form, this is invisible. But I have seen it matter in two scenarios: first, a form inside a large component tree where a single keystroke re-renders hundreds of child components (the fix is memoization or splitting the form into its own component, not switching to uncontrolled). Second, real-time collaborative editing where latency matters — each keystroke round-trips through React state, which introduces a frame of delay. In that case, the editor manages its own DOM state and syncs with React asynchronously.React Hook Form is the hybrid approach: it uses uncontrolled inputs by default (registering refs) but provides a controlled API when needed. This gives you form validation without per-keystroke re-renders. On a form with 50 fields, the difference between 50 re-renders per keystroke (fully controlled) and 0 re-renders per keystroke (React Hook Form) is measurable.Follow-up: If you have a controlled input and the user types very fast, can React “drop” keystrokes? What causes this and how do you fix it?In theory, React’s synchronous state updates in event handlers should process every keystroke. But in practice, if the
onChange handler triggers expensive work (like re-rendering a large component tree or running a complex validation), the browser may feel laggy and the user perceives dropped input.In React 18 with concurrent rendering, if you wrap the state update in startTransition, React can defer the re-render, but the input itself stays responsive because urgent updates (the actual input value) are not deferred. However, if you accidentally put the setValue call inside startTransition, the input value update is deferred and the input appears to “freeze.”The fix for expensive onChange handlers is to separate the input state from the derived computation: update the input value immediately (no transition), and debounce or transition the expensive computation. For example, setQuery(e.target.value) immediately, but startTransition(() => setFilteredResults(expensiveFilter(e.target.value))).How do you architect form validation in React? Compare inline validation, schema validation, and server-side validation patterns.
How do you architect form validation in React? Compare inline validation, schema validation, and server-side validation patterns.
Strong Answer:
There are three layers, and production forms usually need all three:Inline validation runs on every change or blur. It gives immediate feedback for simple rules — required fields, email format, password strength, min/max length. The implementation is straightforward: a
validate function per field that returns an error string or empty. The tradeoff is that complex cross-field validations (password must differ from email, end date must be after start date) require access to the entire form state, which makes per-field validators awkward.Schema validation uses a declarative schema (Yup, Zod, Joi) to define all rules in one place. You validate the entire form object against the schema. This is cleaner for complex forms because cross-field rules are natural, the schema is reusable (server and client), and error messages are centralized. The tradeoff is that validating the entire schema on every keystroke can be wasteful — so libraries like React Hook Form validate only the changed field while still supporting full-schema validation on submit.Server-side validation is the only validation that matters for security. Client-side validation improves UX but can be bypassed. For uniqueness checks (username taken, email already registered), you must hit the server. The pattern: debounce the input, send an async validation request, and display the result. The UX challenge is showing a loading state during the async check and not blocking form submission while waiting.In practice, I use Zod for schema definition (because it gives you TypeScript types for free), React Hook Form for registration and change tracking, and server-side validation as the final gate. The schema is shared between frontend and backend via a shared package, ensuring rules stay in sync.Follow-up: How do you handle displaying server-side validation errors that the client did not catch? For example, “this email is already registered.”The form’s submit handler catches the server error and maps it to the correct field. With React Hook Form, you call setError('email', { type: 'server', message: 'This email is already registered' }). The error displays next to the email field just like a client-side validation error.The UX subtlety: the server error should clear when the user modifies the email field, not persist forever. React Hook Form handles this automatically because setError errors are cleared on the next valid change. For custom implementations, you clear the server error in the field’s onChange handler.For forms with multiple server errors (like a bulk import that returns errors per row), I return a structured error object from the API that maps field names to error messages, then loop through them and set each one. The important thing is that the error format is consistent between client validation and server validation so the UI rendering logic does not care about the error source.What happens when you set a controlled input's value but forget the onChange handler? What does React do, and why?
What happens when you set a controlled input's value but forget the onChange handler? What does React do, and why?
Strong Answer:
React renders the input with the value locked to whatever state holds. When the user types, the browser briefly shows the new character, then React re-renders and resets the input to the unchanged state value. The visual effect is that typing appears to do nothing — the input is “frozen.”React also logs a warning in development: “You provided a
value prop to a form field without an onChange handler. This will render a read-only field.” This warning exists because it is one of the most common form bugs, especially for developers coming from vanilla HTML where inputs manage their own state.The reason this happens is the controlled component contract: by setting value, you tell React “I am the source of truth.” React enforces this by resetting the DOM input value to match the React state on every render. Without onChange to update state, the state never changes, so the input never changes.Two ways to fix it: add an onChange handler that calls setState, or switch to defaultValue if you want the DOM to manage the value (uncontrolled pattern). A readOnly prop suppresses the warning if the read-only behavior is intentional.The gotcha for teams: if someone adds value="" as a placeholder instead of placeholder="...", the input becomes a controlled empty input with no onChange — permanently blank. I have seen this shipped to production.Follow-up: You have a form where one field’s value depends on another (e.g., selecting a country populates a state/province dropdown). How do you handle the cascade without causing double renders?The key is computing the dependent value during render rather than in an effect. When the country changes, the provinces list is derived data: const provinces = countryToProvinces[selectedCountry]. This runs synchronously in the same render — no extra render cycle.For the selected province, you need to reset it when the country changes. The clean way is using a key prop on the province select: <ProvinceSelect key={selectedCountry} ... />. This remounts the component with fresh state. Alternatively, in the country onChange handler, set both values: setCountry(newCountry); setProvince('').Avoid using useEffect(() => { setProvince(''); }, [country]) because it causes a double render: first render with old province and new country, then effect fires and triggers second render with empty province. The user might briefly see an invalid combination. Setting both in the event handler keeps everything in one render cycle thanks to React’s batching.