⚛️ React Frontend Engineering • Track 09

Unit 5: Props and State

Mastering the two fundamental pillars of data flow and reactivity in React: immutable external inputs (props) and reactive internal memory (state). Learn the useState hook, batching mechanics, immutable state updates, lifting state up, and bidirectional component communication.

📚 Module: Unit 5 of 12 Core Units
📖 Core Focus: Props, useState & Lifting State
⚡ Data Flow: Unidirectional & Callbacks
🛡️ Type Safety: Default Values & PropTypes
01

What are Props? The External Contract

In React, Props (short for properties) are the mechanism through which parent components transmit data downward into child components. They represent the configuration arguments or parameters supplied to a component function.

📌 Analogy: Function Arguments in Pure Mathematics

Think of a React component as a pure JavaScript function:
function UserBadge(props) { return <div>{props.name}</div>; }

Just as a regular function receives parameters to determine its output without altering the caller's arguments, a React component receives props from its parent. Props define what the component should display or how it should behave.

Core Axioms of Props

02

Passing & Reading Props in Components

Props are passed into components using JSX attribute syntax. Any valid JavaScript data type can be transmitted as a prop: strings, numbers, booleans, arrays, objects, callback functions, and even other JSX elements.

Passing Diverse Data Types via JSX

Strings can be passed with quotes. All non-string values (numbers, booleans, objects, functions) must be wrapped in curly braces:

ParentComponent.jsx
export default function ParentComponent() {
  function handleSelect(id) {
    console.log("Selected user:", id);
  }

  return (
    <UserCard
      username="ariv_dev"                // String
      reputation={4850}                   // Number
      isStaffMember={true}                // Boolean
      skills={["React", "TS", "Node"]}    // Array
      profile={{ role: "Architect" }}    // Object
      onSelect={() => handleSelect(101)} // Callback Function
    />
  );
}
Reading Props: Two Modern Patterns

React passes all JSX attributes as properties on a single JavaScript object called props:

Pattern 1: The 'props' Object
function UserCard(props) {
  return <h3>{props.username} ({props.reputation})</h3>;
}
Pattern 2: Destructuring (Industry Standard)
// Direct parameter destructuring with default fallbacks
function UserCard({ username, reputation, isStaffMember }) {
  return (
    <div>
      <h3>{username}</h3>
      <span>Score: {reputation}</span>
      {isStaffMember && <span>Staff</span>}
    </div>
  );
}
03

Default Props & Runtime Type Safety with PropTypes

Robust components anticipate missing or malformed inputs. React provides two foundational mechanisms to ensure components receive valid data: default prop fallbacks and runtime type verification.

1. Modern Default Props via ES6 Default Parameters

In modern functional React, default props are defined directly using JavaScript ES6 default parameter assignment. This provides clean, zero-overhead fallbacks when a parent omits a prop:

src/components/AlertBadge.jsx • ES6 Default Parameters
export function AlertBadge({
  message = "Notification received",
  variant = "info",
  dismissible = false
}) {
  return (
    <div className={`alert-badge badge-${variant}`}>
      <span>{message}</span>
      {dismissible && <button>×</button>}
    </div>
  );
}

2. Runtime Type Safety with prop-types

Before TypeScript became pervasive, the official prop-types library was React's primary mechanism for verifying that components receive props of the expected types. In development mode, React logs descriptive console warnings whenever invalid types are detected:

PropTypes Validation Example
import PropTypes from "prop-types";

function MetricCard({ title, value, isTrendingUp, onRefresh }) {
  return (
    <div className="metric-box">
      <h4>{title}</h4>
      <p>{value}</p>
      <span>{isTrendingUp ? "Positive" : "Negative"}</span>
      <button onClick={onRefresh}>Sync</button>
    </div>
  );
}

// Defining the runtime PropTypes contract
MetricCard.propTypes = {
  title: PropTypes.string.isRequired,
  value: PropTypes.number.isRequired,
  isTrendingUp: PropTypes.bool,
  onRefresh: PropTypes.func.isRequired
};

// Optional defaultProps definition (legacy pattern, still supported)
MetricCard.defaultProps = {
  isTrendingUp: true
};
04

What is State? The Component's Private Memory

While props are inputs passed in from the outside, State is a component's personal, private, and internal memory. State represents data that can change over time as a user interacts with the application.

⚠️ Why Regular Local Variables Do Not Work in React

Consider this intuitive attempt using a plain JavaScript variable:

// ❌ BROKEN: Regular variable fails to trigger UI re-renders!
function Counter() {
  let count = 0;

  function handleClick() {
    count = count + 1;
    console.log("Count is now:", count); // Variable increments in console...
  }

  // ...but React NEVER re-renders this JSX on the screen!
  return <button onClick={handleClick}>Clicked {count} times</button>;
}

Regular local variables fail for two reasons:
1. Local variables do not persist between renders: When React re-executes Counter(), it initializes let count = 0 afresh from the beginning.
2. Mutations do not trigger renders: Changing a local variable does not notify React that it needs to reconcile the Virtual DOM and update the browser display.

💡 The React State Solution

React provides the useState Hook to supply components with persistent state variables and an updater function that automatically schedules a re-render whenever called!

05

Architectural Comparison: State versus Props

Understanding the exact distinction between Props and State is one of the most critical conceptual milestones in React engineering:

Dimension Props (Properties) State
Origin & Ownership Passed into the component from its parent Created, declared, and owned inside the component
Mutability Strictly Read-Only (Immutable) Mutable via its updater function (setState)
Who Can Change It? Only the parent component that owns the data The component itself using its updater function
Purpose Configures and customizes child components Tracks dynamic user interaction and time-variant data
Re-render Trigger Child re-renders when parent passes new prop values Component re-renders when setState() is called
Analogy Function arguments passed into a subroutine Internal local variables stored in function memory
06

The useState Hook: Syntax & Anatomy

The useState Hook is a built-in React function that lets functional components retain stateful values across renders.

Syntax Breakdown: Array Destructuring
import { useState } from "react";

// The Hook returns an array with exactly two elements:
// [0]: The current state snapshot
// [1]: The state setter function that triggers re-renders
const [count, setCount] = useState(0);
+--------------------------------------------------------------------------+ | ANATOMY OF A USESTATE HOOK | +--------------------------------------------------------------------------+ const [ userStatus , setUserStatus ] = useState( "offline" ); ^ ^ ^ | | | Current State Value Updater Function Initial State (Read in JSX output) (Called on user event) (Only used on mount)

Lazy State Initialization

If the initial state requires an expensive computation (e.g., parsing a large JSON string or reading from localStorage), pass an initializer function rather than computing the value directly:

Lazy Initializer Pattern
// Efficient: The callback runs ONCE on the initial mount only!
const [authToken, setAuthToken] = useState(() => {
  return localStorage.getItem("app_auth_token") || "guest";
});
07

Updating State Correctly: Batched Updates & Functional Setters

State updates in React do not immediately mutate the current variable in-place. Instead, calling a setter schedules a new render pass. React 18 automatically batches multiple state updates within events to maximize rendering performance.

⚠️ The Stale State Trap: Multiple Direct Calls

Consider what happens when trying to increment a counter 3 times in one event handler:

function handleTripleIncrement() {
  // If count is currently 0:
  setCount(count + 1); // Schedules count to become 0 + 1 = 1
  setCount(count + 1); // Still reads snapshot count = 0! Schedules 0 + 1 = 1
  setCount(count + 1); // Still reads snapshot count = 0! Schedules 0 + 1 = 1
  // RESULT: Count only increments to 1, NOT 3!
}
✅ The Solution: Functional State Updates (Updater Function)

When the new state depends on the previous state value, always pass an updater callback function to setState. React queues each callback and supplies the fresh, calculated intermediate value:

function handleTripleIncrement() {
  // Each call receives the pending state from the previous queue entry
  setCount(prev => prev + 1); // prev is 0 => returns 1
  setCount(prev => prev + 1); // prev is 1 => returns 2
  setCount(prev => prev + 1); // prev is 2 => returns 3
  // RESULT: Count correctly increments by 3!
}
08

Handling Multiple & Complex State Values

In production applications, state is rarely limited to single numbers. React handles both multiple autonomous state hooks and complex data structures (objects and arrays).

1. Multiple Independent States vs. Consolidated State

Best practice: If pieces of state change independently, use separate useState calls. If states always change together (like coordinates x and y, or form fields), group them into an object:

Handling Form State as an Object with Spread Syntax
import { useState } from "react";

export function RegistrationForm() {
  const [formData, setFormData] = useState({
    username: "",
    email: "",
    newsletter: true
  });

  function handleChange(event) {
    const { name, value, type, checked } = event.target;

    // IMMUTABLE UPDATE: Never mutate formData directly!
    // Copy existing fields via spread operator (...) and override the target field
    setFormData(prev => ({
      ...prev,
      [name]: type === "checkbox" ? checked : value
    }));
  }

  return (
    <form>
      <input name="username" value={formData.username} onChange={handleChange} />
      <input name="email" value={formData.email} onChange={handleChange} />
      <input name="newsletter" type="checkbox" checked={formData.newsletter} onChange={handleChange} />
    </form>
  );
}

2. Immutable Array State Operations

Never use mutating array methods like push(), pop(), splice(), or sort() on state arrays. Instead, use non-mutating methods that return brand-new array references:

Operation Mutating (Avoid in React ❌) Immutable Replacement (Preferred in React ✅)
Adding an item arr.push(newItem) [...arr, newItem] (Spread syntax)
Removing an item arr.splice(index, 1) arr.filter(item => item.id !== targetId)
Updating an item arr[index].name = "Updated" arr.map(item => item.id === id ? { ...item, name: "Updated" } : item)
Sorting an array arr.sort() [...arr].sort() (Copy first, then sort)
09

Lifting State Up & Bidirectional Component Communication

In React, data flows downward, but sibling components often need to coordinate or reflect the same changing data.

📌 What is "Lifting State Up"?

When two or more sibling components need access to the same state, you lift the state up to their closest common parent component.

1. The parent holds the state via useState.
2. The parent passes the current state value down to siblings as props.
3. The parent passes an updater callback function down to any child that needs to request a state change.

+--------------------------------------------------------------------------+ | LIFTING STATE UP & COMPONENT COMMUNICATION | +--------------------------------------------------------------------------+ [ CommonParent ] | Holds State: [query, setQuery] | +------------------+------------------+ | | | Passes: | Passes: | - query (prop) | - query (prop) | - onQueryChange={setQuery} | v v [ SearchBarInput ] [ FilteredResultsList ] (Calls callback when user types) (Re-renders when query changes)

Bidirectional Communication Pattern

10

Complete Standalone Practical Implementation

Below is a fully functional, production-structured shopping cart component showcasing props passing, default props, useState, functional state updates, array immutability, and lifting state up:

src/ShoppingCartApp.jsx • End-to-End Reactive System
import { useState } from "react";

// Child Component 1: Product Row with Event Callbacks and Default Props
function CartItem({ item, onIncrement, onDecrement, onRemove, currency = "USD" }) {
  const lineTotal = item.price * item.quantity;

  return (
    <div style={{
      display: "flex",
      justifyContent: "space-between",
      alignItems: "center",
      padding: "10px 0",
      borderBottom: "1px solid #e2e8f0"
    }}>
      <div>
        <strong style={{ color: "#0f172a" }}>{item.name}</strong>
        <div style={{ fontSize: "12px", color: "#64748b" }}>
          {currency} {item.price.toFixed(2)} each
        </div>
      </div>

      <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
        <button onClick={() => onDecrement(item.id)} style={btnStyle}>-</button>
        <span style={{ fontWeight: 600, minWidth: "24px", textAlign: "center" }}>{item.quantity}</span>
        <button onClick={() => onIncrement(item.id)} style={btnStyle}>+</button>
        <span style={{ fontWeight: 700, minWidth: "70px", textAlign: "right" }}>
          {currency} {lineTotal.toFixed(2)}
        </span>
        <button onClick={() => onRemove(item.id)} style={{ ...btnStyle, color: "#ef4444" }}>×</button>
      </div>
    </div>
  );
}

// Child Component 2: Summary Card Displaying Lifted State
function CartSummary({ totalAmount, itemCount, onClearCart }) {
  return (
    <div style={{ background: "#f8fafc", padding: "16px", borderRadius: "8px", marginTop: "16px" }}>
      <div style={{ display: "flex", justifyContent: "space-between", marginBottom: "8px" }}>
        <span>Total Units:</span>
        <strong>{itemCount}</strong>
      </div>
      <div style={{ display: "flex", justifyContent: "space-between", fontSize: "16px", marginBottom: "12px" }}>
        <span>Grand Total:</span>
        <strong style={{ color: "#0284c7" }}>USD {totalAmount.toFixed(2)}</strong>
      </div>
      <button onClick={onClearCart} style={{ ...btnStyle, width: "100%", padding: "8px" }}>
        Clear Shopping Cart
      </button>
    </div>
  );
}

// Parent Component: Holds and Lifts State
export default function ShoppingCartApp() {
  const [cart, setCart] = useState([
    { id: 1, name: "React System Design Course", price: 49.99, quantity: 1 },
    { id: 2, name: "TypeScript Deep Dive Handbook", price: 29.99, quantity: 2 }
  ]);

  // Handler: Functional State Update with Array Map
  function handleIncrement(id) {
    setCart(prev =>
      prev.map(item => item.id === id ? { ...item, quantity: item.quantity + 1 } : item)
    );
  }

  // Handler: Decrement with Lower Bound Guard
  function handleDecrement(id) {
    setCart(prev =>
      prev.map(item =>
        item.id === id && item.quantity > 1
          ? { ...item, quantity: item.quantity - 1 }
          : item
      )
    );
  }

  // Handler: Immutable Removal with Array Filter
  function handleRemove(id) {
    setCart(prev => prev.filter(item => item.id !== id));
  }

  // Computed Derived State (No separate state variable needed!)
  const totalAmount = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
  const totalItems = cart.reduce((sum, item) => sum + item.quantity, 0);

  return (
    <div style={{ maxWidth: "560px", margin: "24px auto", border: "1px solid #e2e8f0", borderRadius: "12px", padding: "24px" }}>
      <h2 style={{ marginBottom: "16px", color: "#0f172a" }}>DevAscent Academy Store</h2>

      {cart.length === 0 ? (
        <p style={{ color: "#64748b" }}>Your cart is completely empty.</p>
      ) : (
        cart.map(item => (
          <CartItem
            key={item.id}
            item={item}
            onIncrement={handleIncrement}
            onDecrement={handleDecrement}
            onRemove={handleRemove}
          />
        ))
      )}

      {cart.length > 0 && (
        <CartSummary
          totalAmount={totalAmount}
          itemCount={totalItems}
          onClearCart={() => setCart([])}
        />
      )}
    </div>
  );
}

const btnStyle = {
  background: "#ffffff",
  border: "1px solid #cbd5e1",
  borderRadius: "6px",
  padding: "4px 10px",
  cursor: "pointer",
  fontWeight: 600
};
11

Academic Viva Voce & Technical Interview Masterclass

Q1: What is the fundamental difference between Props and State in React?

Answer: Props are external, read-only parameters passed from a parent component to configure child components, obeying strict unidirectional data flow and immutability. In contrast, State is an internal, private data store managed within the component itself using hooks (e.g., useState). While props cannot be modified by the receiving component, state can be updated via its setter function, which triggers a reconciliation pass and updates the DOM.

Q2: Why does mutating state directly (e.g., state.count = 5) fail to update the user interface?

Answer: React detects updates through reference checks in its reconciliation pipeline. Direct variable mutation alters memory values behind React's back without invoking the state updater function. As a result, React is never notified that state has changed, no re-render is scheduled, and the Virtual DOM does not update the browser interface. Always invoke the updater function (e.g., setCount(5)) to notify React.

Q3: When and why must you use a functional state update (e.g., setCount(prev => prev + 1))?

Answer: Functional state updates are mandatory whenever the next state depends on the previous state. Because React batches multiple state updates together and executes them asynchronously, reading the state variable directly from the current render closure can produce stale snapshot values. Passing an updater callback prev => prev + 1 ensures that React supplies the most up-to-date pending state from the update queue.

Q4: What does "Lifting State Up" mean, and what problem does it solve?

Answer: "Lifting state up" refers to relocating a piece of state from a local child component to the nearest common ancestor of two or more sibling components that both need access to that data. It solves the problem of keeping sibling components in sync under React's unidirectional data flow: the parent owns the single source of truth, distributes the data down as props, and receives change requests via callback functions.

Q5: How should you handle calculated values in React (e.g., total price of items in a cart)?

Answer: Avoid storing redundant or calculated values in state! If a value can be computed or derived from existing props or state during the render cycle (such as const total = items.reduce(...)), compute it directly in the component body. Storing derived state in separate useState variables introduces synchronization bugs, requires redundant effects, and adds unnecessary render overhead.

Q6: Why are ES6 default parameters preferred over the legacy defaultProps object in modern React?

Answer: Modern functional components are plain JavaScript functions. Using standard ES6 parameter defaults (e.g., function Card({ title = "Default" })) is standard JavaScript syntax, eliminates extra library baggage, integrates seamlessly with TypeScript type inference, and avoids the runtime overhead associated with the legacy Component.defaultProps pattern, which the React core team has formally slated for deprecation.

12

Unit Summary & Key Conceptual Takeaways

Core Principles to Retain
  • Props are Immutable: External configurations passed from parent to child; components must treat them as strictly read-only.
  • State is Private & Reactive: Internal component memory managed via useState; updating state schedules an automatic UI re-render.
  • Always Use Functional Updates for Dependent State: Use setVal(prev => prev + 1) to avoid stale closure state bugs.
  • Never Mutate Objects or Arrays: Always copy via spread operators ({ ...prev, key: value }) or immutable methods (filter, map, concat).
  • Derive, Don't Duplicate: If a value can be computed from existing props or state, calculate it on the fly rather than creating a new state variable.
  • Lift State for Sibling Synchronization: Place shared state in the nearest common parent and pass down props and callback updaters.