Advanced Web Programming

React State, Events, Forms and Shared State

PGCP-AC

State lets a component remember information between renders. Events request changes, forms connect browser controls to application data and lifting state gives collaborating components one authoritative owner. Correct state design depends on snapshots, immutable updates and minimal stored data.

1. State and render snapshots

State is a component's memory between renders. A state setter requests an update; it does not rewrite values captured by the currently executing render. When the next value depends on the previous one, a functional updater expresses that dependency. Hooks are called at the top level of function components or custom hooks, preserving their call order. Local variables alone do not provide React with persistent state and a rerender signal. These rules are described in the useState reference.

Objects and arrays in state should be replaced rather than mutated in place. Copy the changed nesting path so that previous state remains intact. This is especially important when several components share references. Event handlers are passed as functions, for example onClick={handleClick}; writing onClick={handleClick()} invokes the function during rendering.

2. Forms and ownership

A controlled input receives its displayed value from state and updates that state through an event handler. A checkbox uses checked state. An uncontrolled input keeps its value in the DOM and can be read through a ref. Lift shared state to the closest common owner so sibling components agree on one source of truth. Context carries a value through a component subtree without manually forwarding props at every intermediate level; it does not remove the need for clear state ownership.

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

Refs retain a mutable value between renders without causing a render when that value changes. They are useful for focusing an input or keeping an external handle. Derived values should usually be calculated from existing state instead of stored redundantly. Redundant state creates synchronization problems, such as a total that no longer matches its list of items.

3. A controlled form in detail

function NameForm() {
  const [name, setName] = useState('');
  function submit(event) {
    event.preventDefault();
    if (name.trim()) console.log(name.trim());
  }
  return <form onSubmit={submit}>
    <label>Name <input value={name}
      onChange={event => setName(event.target.value)} /></label>
    <button type="submit">Save</button>
  </form>;
}

The displayed value comes from name and the change handler updates that value for the next render. Submitting by keyboard follows the form's submit path, so the behavior does not depend solely on a button click. The initial empty string keeps the field controlled from the beginning. For a checkbox, use checked and read event.target.checked. When two components edit the same value, put it in their common owner and pass each the value and an appropriate change callback.

4. State belongs to a tree position

Calling useState gives the current render a state snapshot and a setter. React preserves that state while the same component type remains at the same tree position. Removing the component or changing its key can discard that state.

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

The count variable does not change inside the running event handler. Calling the setter requests another render with a new snapshot. Code later in the same handler still sees the snapshot captured when that handler was created.

Local variables do not serve as component memory: they are recreated during rendering and changing one does not request a render. Module-level variables persist but are shared outside component ownership and can cause users or instances to interfere with one another.

5. Batching and functional updaters

React can batch state requests from an event before rendering. These three calls all calculate from the same captured value:

setCount(count + 1);
setCount(count + 1);
setCount(count + 1);

When each update depends on the preceding queued result, supply an updater:

setCount(value => value + 1);
setCount(value => value + 1);
setCount(value => value + 1);

React queues the updater functions and passes each the result of the previous one. Use a direct replacement when the next value is independent and a functional updater when it is calculated from previous state. Updaters should themselves be pure.

6. Hook call rules

Call Hooks at the top level of a function component or custom Hook, not inside ordinary conditions, loops, nested functions or event handlers. React associates Hook state with call order.

function Profile({ enabled }) {
  const [name, setName] = useState('');
  if (!enabled) return null;
  // useState remains before the conditional return
}

Custom Hooks can compose stateful behavior and are named with use. Calling the same custom Hook twice creates two independent instances of its state unless both connect to the same external system.

7. Event handlers

Pass a function as the handler prop:

<button onClick={handleSave}>Save</button>
<button onClick={() => removeCourse(course.id)}>Remove</button>

onClick={handleSave()} invokes the function during render and passes its result as the handler. An inline arrow defers the call and can supply arguments. Handlers can read the current render's props and state, call setters and perform user-triggered side effects.

React event handlers receive an event object. Use the form's onSubmit for submission so keyboard and button submission follow the same logic. Default cancellation does not stop propagation and propagation control does not cancel default behavior.

8. Immutable object updates

State is treated as an immutable snapshot. Do not mutate an existing object and pass the same reference back.

const [student, setStudent] = useState({
  name: 'Asha',
  address: { city: 'Pune', pin: '411001' }
});

setStudent(current => ({
  ...current,
  address: { ...current.address, city: 'Mumbai' }
}));

Every changed nesting level is copied. Unchanged values can be shared. Shallow spread does not recursively clone, so copying only the outer student and mutating address.city would still alter the earlier nested object.

9. Immutable array updates

Create a new array for insertion, removal, replacement, sorting and reversal:

setCourses(current => [
  ...current,
  { id: crypto.randomUUID(), title: newTitle }
]);

setCourses(current => current.filter(course => course.id !== removedId));

setCourses(current => current.map(course =>
  course.id === changed.id ? { ...course, title: changed.title } : course
));

push, splice, sort and reverse mutate their receiver. Copy before using a mutating ordering method. Creating a new outer array is insufficient if an existing element object is then mutated; copy the changed element too.

10. Controlled inputs

A controlled field receives its current display state from React and reports edits through a handler.

function CourseForm({ onSave }) {
  const [form, setForm] = useState({ title: '', level: 'beginner', active: false });

  function change(event) {
    const { name, type, value, checked } = event.target;
    setForm(current => ({
      ...current,
      [name]: type === 'checkbox' ? checked : value
    }));
  }

  function submit(event) {
    event.preventDefault();
    const title = form.title.trim();
    if (title) onSave({ ...form, title });
  }

  return (
    <form onSubmit={submit}>
      <label>Title <input name="title" value={form.title} onChange={change} /></label>
      <label>Level <select name="level" value={form.level} onChange={change}>
        <option value="beginner">Beginner</option>
        <option value="advanced">Advanced</option>
      </select></label>
      <label><input name="active" type="checkbox" checked={form.active} onChange={change} /> Active</label>
      <button type="submit">Save</button>
    </form>
  );
}

Text and select controls use value; checkboxes use checked. Initialize text fields to a string and checkbox fields to a boolean so they remain controlled throughout their lifetime.

11. Uncontrolled inputs and refs

An uncontrolled input keeps its current value in the DOM. defaultValue or defaultChecked supplies the initial value and a ref can read it when necessary.

function UploadForm() {
  const fileRef = useRef(null);
  function submit(event) {
    event.preventDefault();
    const file = fileRef.current.files[0];
    if (file) upload(file);
  }
  return <form onSubmit={submit}><input ref={fileRef} type="file" /><button>Upload</button></form>;
}

File inputs are commonly uncontrolled because browsers restrict programmatic file selection. Refs also support focus, measurements, timers and external-library handles. Changing ref.current does not request a render; data needed for visual output belongs in state.

12. Lifting shared state

When siblings must reflect the same value, move it to their closest common owner and pass the value and update intentions downward.

function TemperatureCalculator() {
  const [celsius, setCelsius] = useState('');
  const fahrenheit = celsius === '' ? '' : Number(celsius) * 9 / 5 + 32;
  return <>
    <TemperatureInput label="Celsius" value={celsius} onChange={setCelsius} />
    <output>{fahrenheit === '' ? '—' : `${fahrenheit} °F`}</output>
  </>;
}

Celsius is the single stored source. Fahrenheit is calculated during render. Storing both values would require synchronization and invite contradictions.

13. Choosing minimal state

Store the smallest information needed to reconstruct the interface. Avoid storing:

  • values calculable from props or state;
  • filtered or sorted copies that can be calculated for rendering;
  • the same entity in several disconnected places;
  • state used only to mirror a prop without deliberate independence.

State should be normalized enough that one update has one clear location. Store a selected record ID rather than a copied selected object when the record already lives in a collection; derive the selected record from the ID and collection.

14. Context and ownership

Context lets an owner provide a value to a descendant subtree without forwarding it through every intermediate component. It suits values such as theme, authenticated identity, locale or a domain service that many descendants need.

Context does not decide what should be state, make updates free or replace thoughtful component boundaries. Consumers rerender when the provided value changes. Split unrelated values and keep provider values stable where this materially improves behavior.

15. Reducers for coordinated transitions

When many handlers perform related updates, a reducer can centralize transitions:

function cartReducer(state, action) {
  switch (action.type) {
    case 'added':
      return [...state, action.item];
    case 'removed':
      return state.filter(item => item.id !== action.id);
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

const [cart, dispatch] = useReducer(cartReducer, []);

A reducer receives current state and an action and returns the next state without mutation. It does not automatically make state global. useReducer is useful when actions describe domain transitions more clearly than scattered setter logic.

16. Form validation and submission state

Keep raw editable text when partial input is meaningful—for example, a user may temporarily type - before a number. Validate at an appropriate time and convert at the boundary. Distinguish field errors, submission failure, pending state and success.

During async submission, prevent harmful duplicate operations, keep errors associated with fields, preserve user entries after failure and handle a component disappearing or a later request superseding an earlier one. Client validation improves experience; the server remains authoritative.

17. Practical considerations

  1. A setter requests a later render; it does not rewrite the current snapshot.
  2. Use functional updaters for queued changes based on previous state.
  3. Hooks must keep a stable top-level call order.
  4. Pass event handlers as functions rather than invoking them during render.
  5. Controlled text fields use value; checkboxes use checked.
  6. State objects and arrays should be replaced, not mutated.
  7. Copy every changed nesting level.
  8. Ref changes do not trigger rendering.
  9. Lift shared data to the closest common owner.
  10. Calculate derived data rather than storing synchronized duplicates.

Continue learning

Related notes

Put this topic into timed practice

Open mock tests when you want full-exam pacing, or keep drilling in practice mode.