Unit 3: State, Events, Hooks, and Side Effects

INT252 — Web App Development With Reactjs 11 min read

I. React’s State-Driven Component Model

React applications are built from components whose rendered output is determined by current props, state, and context. Events request state changes, React re-renders affected components, and effects synchronize the resulting interface with systems outside React.

  • Declarative rendering: A component describes what the UI should look like for the current data rather than manually changing individual DOM nodes.
  • Render snapshot: Each render receives a fixed snapshot of props and state; event handlers created during that render capture those values.
  • State ownership: State should be stored in the nearest component that must control it and passed downward through props or context.
  • One-way data flow: Data normally flows from parent to child, while child components communicate upward by calling callback props.
  • Pure rendering: Given identical props, state, and context, a component should calculate identical JSX without causing side effects.
  • Commit and effects: React first calculates and commits DOM updates; effects then synchronize with APIs such as timers, networks, or browser events.
  • Hook convention: Hooks are functions beginning with use, such as useState, and provide React features to function components.

II. Events and Component State — Producing Interactive Interfaces

React event handlers respond to user or browser activity, while local state preserves information that must influence later renders.

A. Event handling mechanisms in React

React handles events by assigning callback functions to JSX event props using camelCase names such as onClick and onChange.

  • Function reference: Pass a function rather than calling it during rendering.
JSX
function SaveButton() {
  function handleClick() {
    console.log("Saved");
  }

  return <button onClick={handleClick}>Save</button>;
}
  • Event object: A handler receives a React event object with information such as event.target.value; React events provide a consistent interface across browsers.
  • Passing arguments: An arrow function can supply additional values: onClick={() => removeItem(id)}.
  • Default behavior: event.preventDefault() prevents actions such as a form submission reloading the page.
  • Propagation: Events normally bubble from a child to its ancestors; event.stopPropagation() stops that propagation.
  • Handler naming: Names such as handleSubmit describe internal handlers, while custom component props commonly use names such as onSave.

B. Local state management using useState

The useState Hook stores component-local data whose changes must be reflected in the rendered interface.

  • Declaration: useState(initialValue) returns the current state and a setter.
JSX
const [count, setCount] = useState(0);
  • Defined names: count is the current state snapshot, setCount schedules its replacement, and 0 is the initial value used on the first render.
  • Persistence: State survives re-renders but belongs to a particular component instance and position in the component tree.
  • Lazy initialization: For expensive initialization, pass a function—useState(createInitialData)—so React calls it only during initialization.
  • Objects and arrays: Replace rather than mutate state: setUser({...user, name: "Asha"}) creates a new object identity.
  • State isolation: Rendering <Counter /> twice creates two independent state stores.

C. State update behavior and batching

State setters schedule future renders rather than immediately changing the state variable in the currently executing code.

  • Snapshot behavior: After setCount(count + 1), count retains its old value until the existing handler finishes.
  • Updater function: When the next value depends on the previous one, use setCount(c => c + 1), where c is the latest queued value.
  • Queued calculations: Three calls to setCount(c => c + 1) increase the count by three; three calls to setCount(count + 1) usually request the same replacement value.
  • Batching: Modern React groups multiple state updates occurring within the same task and commonly performs one resulting render, improving efficiency.
  • Equality check: React may skip an update when the new state is identical to the old state according to Object.is.

D. Re-render triggers and optimization awareness

A component re-renders when its state changes, its parent renders it again, or a context value it consumes changes.

  • Re-render scope: Rendering a parent normally causes React to call its child components, although unchanged DOM nodes are not necessarily rewritten.
  • Reconciliation: React compares the new element tree with the previous one and commits only required DOM changes.
  • Identity sensitivity: New object, array, and function literals receive new references on every render, which matters to memoized children and Hook dependencies.
  • Measured optimization: Developers should profile before optimizing; ordinary re-renders are expected and are not automatically performance defects.
  • Component memoization: React.memo(Component) may skip rendering when props are shallowly unchanged, but state and consumed context can still trigger rendering.

E. Derived state concepts

Derived state is information that can be calculated from existing props or state during rendering and therefore often should not be stored separately.

  • Direct calculation: With firstName and lastName in state, calculate const fullName = firstName + " " + lastName.
  • Consistency: Avoiding a separate fullName state variable prevents it from becoming out of sync with its sources.
  • Filtering example: Derive visibleItems with items.filter(...); use useMemo only if profiling shows that the calculation is expensive.
  • Reset by identity: A changed key, such as <Profile key={userId} />, gives a component a new identity and resets its local state.
  • Legitimate stored state: Store data that changes independently, such as user-edited input, rather than values fully determined by existing data.

F. State management best practices

Effective state design keeps data minimal, immutable, correctly owned, and easy to update.

  • Single source of truth: Each piece of state should have one authoritative owner; shared state is commonly lifted to the nearest common ancestor.
  • Minimal representation: Store essential values and derive totals, labels, or filtered collections during rendering.
  • Immutability: Use map, filter, spread syntax, or libraries that preserve immutable update semantics instead of changing existing values.
  • Suitable structure: Avoid deeply nested state when a flatter model makes updates clearer.
  • Functional updates: Use updater functions for changes based on previous state, especially when several updates may be queued.
  • State boundaries: Keep transient state, such as whether one menu is open, close to the component that uses it; use broader context only for genuinely shared data.

III. Hooks and Effects — Connecting Components to React and External Systems

Hooks attach state, context, lifecycle synchronization, and reusable behavior to function components; effects are specifically intended for synchronization outside pure rendering.

A. Built-in hooks including useState, useEffect, and useContext

React’s built-in Hooks address distinct component requirements and should be chosen according to the source of data or behavior.

  • useState: Stores local values and returns [state, setState].
  • useEffect: Runs synchronization logic after React commits a render and may return a cleanup function.
  • useContext: Reads the nearest matching provider’s value.
JSX
const theme = useContext(ThemeContext);
  • Context updates: When the provider supplies a changed value, consumers re-render; context avoids repeated prop passing but does not replace all state management.
  • Provider pattern: A parent can expose data with <ThemeContext.Provider value={theme}>.
  • Hook composition: A component can call several Hooks, with React associating their state by consistent call order.

B. Managing side effects using useEffect

useEffect synchronizes a component with external systems such as subscriptions, timers, browser APIs, or network connections.

  • Effect structure: The setup function runs after a committed render.
JSX
useEffect(() => {
  document.title = `Count: ${count}`;
}, [count]);
  • Appropriate uses: Connecting to a chat server, registering a DOM listener, starting a timer, or synchronizing a non-React widget are effects.
  • Inappropriate uses: Formatting data for JSX and responding directly to a click should usually occur during rendering and in the event handler, respectively.
  • Data fetching: Effects can fetch data, but code should handle stale responses, cancellation, loading states, and framework-provided fetching alternatives.
  • Development checks: Strict Mode may run setup, cleanup, and setup again in development to reveal effects that do not clean up correctly.

C. Dependency array semantics and effect cleanup

The dependency array determines when an effect is re-synchronized, while cleanup reverses the previous synchronization.

  1. Dependency behavior:

    • No array: useEffect(fn) runs after every committed render.
    • Empty array: useEffect(fn, []) runs after mounting, with development Strict Mode checks still possible.
    • Listed values: useEffect(fn, [roomId]) runs initially and whenever roomId changes by Object.is comparison.
    • Reactive dependencies: Props, state, and component-scope values read by the effect must normally be listed.
  2. Cleanup behavior:

    • Timing: Before rerunning an effect, React executes the previous cleanup; cleanup also runs when the component unmounts.
    • Symmetry: A timer created by setInterval should be removed with clearInterval, and an event listener should be removed with the same event type and callback.
    • Stale closures: Omitting dependencies can make an effect continue using values captured from an old render; dependencies should not be suppressed merely to control timing.

IV. Optimization, Reuse, and Hook Discipline

Memoization and custom Hooks can improve selected designs, but correctness, clear data flow, and compliance with Hook rules remain primary.

A. Performance optimization using useCallback and useMemo

useCallback memoizes a function reference, whereas useMemo memoizes the result of a calculation between renders.

  1. useCallback:

    • Form: useCallback(() => save(id), [id]) returns the same function identity until id changes.
    • Use case: It can prevent unnecessary renders when the function is passed to a React.memo child or used as another Hook’s dependency.
  2. useMemo:

    • Form: useMemo(() => sortItems(items), [items]) caches the calculated sorted result until items changes.
    • Use case: It is appropriate for measurably expensive calculations or stable derived references needed by memoized children.
  • Limitation: Both are performance tools, not correctness guarantees; excessive memoization adds dependency management, memory use, and complexity.
  • Dependency accuracy: Every reactive value used inside the callback or calculation must be included.

B. Introduction to custom hooks for logic reuse

A custom Hook is a function beginning with use that combines Hooks to reuse stateful logic across components.

  • Example structure:
JSX
function useOnlineStatus() {
  const [online, setOnline] = useState(navigator.onLine);
  // Effects can subscribe to browser online/offline events.
  return online;
}
  • Logic rather than state sharing: Two components calling useOnlineStatus() receive independent Hook state, although both reuse the same subscription pattern.
  • Encapsulation: A custom Hook can hide effect setup, cleanup, and state transitions behind a focused API.
  • Naming contract: The use prefix signals that Hook rules apply and enables linting tools to inspect calls.
  • Design goal: Custom Hooks should express a clear purpose, such as useWindowWidth or useChatConnection, rather than acting as arbitrary utility functions.

C. Rules of hooks

Hooks must be called in a stable order so React can associate each call with the correct internal state.

  • Top-level rule: Call Hooks only at the top level, not inside conditions, loops, nested functions, event handlers, or try blocks.
  • Valid callers: Call Hooks only from React function components or other custom Hooks.
  • Stable order: A conditional call such as if (loggedIn) useEffect(...) changes call order between renders and can associate state with the wrong Hook.
  • Correct condition placement: Call the Hook unconditionally and place the condition inside it or return early only after all required Hooks have run.
  • Tooling: React’s ESLint Hook rules detect invalid calls and missing effect dependencies.

D. Common pitfalls and anti-patterns

Most Hook-related defects arise from mutation, redundant state, stale closures, uncontrolled effects, or premature optimization.

  • Calling handlers during render: onClick={save()} executes immediately; use onClick={save} or an arrow function.
  • Mutating state: items.push(item) preserves the old array reference; use setItems(old => [...old, item]).
  • Redundant state: Storing values such as totals that can be calculated from current items creates synchronization bugs.
  • Effect loops: An effect that updates a dependency on every run can repeatedly render; reconsider whether the effect is necessary or constrain the update.
  • Missing cleanup: Unremoved listeners, subscriptions, and timers can cause duplicated behavior and resource leaks.
  • Unstable dependencies: Objects or functions created during every render can rerun effects unnecessarily; move them inside the effect, simplify dependencies, or memoize only when justified.
  • Using effects for events: A purchase request caused by a button click belongs in the click handler, not in an effect watching a clicked flag.
  • Misusing context: Frequently changing large context values can re-render many consumers; split contexts or keep local data local.
  • Premature memoization: Applying useMemo and useCallback everywhere can obscure code without producing measurable improvement.