React

Props & State

15 Questions

Props are external data passed from a parent component — they are strictly read-only and a component must never modify them. State is internal data managed within a component that can change over time in response to user actions or events. Props configure a component's appearance and behavior from the outside, while state tracks what changes within it. When either props or state changes, React re-renders the component to reflect the new values.
function Counter({ initialCount }) {   // initialCount is a prop
  const [count, setCount] = useState(initialCount); // count is state
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Props flow down from parent to child. State is local and triggers re-renders when updated via its setter function.

Why it matters: Understanding the difference between props and state is the foundation of React. Props are data passed in from outside; state is data the component itself owns and controls. Getting this wrong leads to broken data flow and bugs.

Real applications: A Button component receives its label as a prop (controlled by parent), while a form field's current value is stored in state (managed internally). Most components use both together.

Common mistakes: Storing props in state (causes stale values when props change), mutating state directly instead of using setState, and not knowing that state updates are async (reading state right after setting it may return the old value).

Props are specified as JSX attributes on a child element in the parent's render output, and received as a single object argument in the child's function. In practice, that object is almost always destructured in the function signature for cleaner access to individual values. Any JavaScript value can be a prop — strings, numbers, objects, arrays, functions, or even other React elements. When passing a non-string prop, wrap the value in curly braces: count={42} not count="42".
function Parent() {
  return <Child name="Alice" age={30} />;
}

function Child({ name, age }) {
  return <p>{name} is {age}</p>;
}
You can pass any JavaScript value as a prop: strings, numbers, objects, arrays, functions, or even other components.

Why it matters: Passing props is how components communicate. The parent controls the child's behavior entirely through props, keeping the app's data flowing in one direction — which makes it easier to trace and debug.

Real applications: Passing a user object to a ProfileCard, passing a list of items to a Table component, passing an onSubmit callback from a page to a Form, and passing a component as a prop to render inside a layout slot.

Common mistakes: Passing too many individual props when an object would be cleaner, forgetting to destructure and writing props.name everywhere, and not using spread carefully ({...props} passes everything including unwanted props to DOM elements).

React detects state changes by performing a reference comparison — it checks whether the new value is a different object in memory, not whether the contents changed. Mutating state directly (e.g., pushing to an array or changing an object property in place) does not create a new reference, so React skips the re-render entirely since it sees no change. The correct approach is to always create a new object or array when updating state — use the spread operator, map, filter, or Object.assign to produce a new value. This immutability rule is fundamental to React's predictable rendering model.
// Wrong — mutating directly
user.name = "Bob";
setUser(user); // same reference, no re-render

// Correct — creating a new object
setUser({ ...user, name: "Bob" }); // new reference, triggers re-render
Always create new objects or arrays when updating state. This enables React's diffing algorithm and predictable rendering behavior.

Why it matters: React uses reference equality to detect state changes. If you mutate the existing object, the reference stays the same, and React does not re-render. Immutability makes change detection reliable and renders predictable.

Real applications: Updating a list by spreading into a new array, adding a property to an object by creating a new object with spread, and removing an item with .filter() rather than .splice().

Common mistakes: Calling state.push(item) and then setting state (React doesn't see the change), directly setting state.name = 'Alice' without using the setter, and mutating nested objects (state.user.name = 'Alice' — you must spread all the way down).

Lifting state up is the pattern of moving shared state to the closest common ancestor of the components that need it, so they all work from the same source of truth. In React, data flows one way — downward through props — so when two sibling components need to share a value, that value must live in a parent above both of them. The parent holds the state and passes both the current value and an updater callback down as props to its children. This keeps the data in one place and prevents sibling components from getting out of sync with each other.
function Parent() {
  const [value, setValue] = useState("");
  return (
    <>
      <Input value={value} onChange={setValue} />
      <Display value={value} />
    </>
  );
}
This pattern keeps components in sync. The parent owns the state and passes it down along with update callbacks as props.

Why it matters: When two sibling components need to share data, neither should own the state independently. Moving state to their common parent is the React-idiomatic way to keep them in sync without an external library.

Real applications: A search input and a results list that both respond to the search query, two form steps that share form data, a filter panel and a product grid controlled by the same filter values.

Common mistakes: Lifting state too far up (makes the root component bloated), not lifting state high enough (siblings get out of sync), and lifting state when Context or a state manager would be a cleaner solution for deeply nested components.

Prop drilling occurs when you need to pass data through multiple layers of intermediate components that don't actually use it — they only pass it further down to their children. This creates tight coupling between unrelated components and clutters component signatures with props they don't care about. For shallow trees, prop drilling is acceptable and often the simplest approach. For deep trees, the standard solutions are React Context for broadly shared values or a state management library like Redux or Zustand for complex global state.
// Prop drilling: App -> Layout -> Sidebar -> UserMenu -> Avatar
<Layout user={user}>   // Layout doesn't use user
  <Sidebar user={user}> // Sidebar doesn't use user
    <UserMenu user={user} />
  </Sidebar>
</Layout>
Solutions include React Context API, state management libraries like Redux, or component composition patterns to avoid unnecessary nesting.

Why it matters: Prop drilling makes code hard to maintain — every intermediate component must accept and forward props it doesn't use. Recognizing when it becomes a problem and knowing the right solution is an important skill.

Real applications: Passing a current user object from the App root down through multiple levels to a Navbar component, threading a theme or locale through an entire component tree, passing callbacks many levels deep.

Common mistakes: Using Context to fix all prop drilling (Context is great for global data but causes all consumers to re-render on change), not trying component composition first (can solve many cases without any state solution), and creating large Context providers with unrelated data.

The children prop is a special built-in prop that automatically contains whatever JSX is placed between a component's opening and closing tags. This enables the composition pattern — you can build generic wrapper components like cards, modals, and layouts that are completely agnostic about what content they'll display. You don't declare children in the component's prop list; React passes it automatically whenever anything is nested between the tags. Any valid React content works as children — text, elements, arrays, or other components.
function Card({ children, title }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div>{children}</div>
    </div>
  );
}

<Card title="Info"><p>Content here</p></Card>
The children prop enables flexible component composition by letting parents inject arbitrary content into a component's layout.

Why it matters: The children prop is what makes React components truly composable. Layout components like modals, cards, and panels become generic and reusable by accepting children instead of specifying their content directly.

Real applications: A Card component that renders any content inside its borders, a Modal that wraps any dialog content, a Layout component with a header and footer that wraps page-specific content, and a Button with flexible label via children.

Common mistakes: Not using children and instead accepting a content or label prop when children would be more flexible, not knowing you can use React.Children utilities to map or count children, and passing multiple children when a component only expects one (need to wrap in a Fragment).

Using default parameter values in the function signature is the modern way to define fallback values for optional props in functional components. When a parent omits a prop or passes undefined, the default value activates and the component renders as expected without crashing or showing empty content. This is purely standard JavaScript — no React-specific API needed — making the component easy to understand and test in isolation. Default values also serve as implicit documentation: they show exactly which props are optional and what safe fallback behaviors look like.
function Alert({ message = "Something happened", type = "info" }) {
  return <div className={`alert alert-${type}`}>{message}</div>;
}

// Usage
<Alert />                    // uses both defaults
<Alert message="Error!" type="danger" />  // overrides both
Default parameter values are evaluated when the prop is undefined, not when it is null.

Why it matters: Default props make components more robust when some data is optional. They clearly communicate what a component needs vs what is optional, and prevent undefined-related rendering errors.

Real applications: A pagination component with default pageSize={10}, a dialog with default isOpen={false}, a button with default variant="primary", and any reusable component that works sensibly without all props being provided.

Common mistakes: Using prop || default when 0 or false are valid values (use prop ?? default instead), not providing defaults for props that affect rendered output (blank UI is confusing), and using the legacy Component.defaultProps in modern functional component code.

PropTypes is a lightweight runtime type-checking library that validates the shape and types of props at development time. When a component receives props that don't match the declared types, PropTypes logs a warning in the browser console without throwing an error, helping developers catch issues early. For new projects, TypeScript is the modern standard — it provides stronger compile-time type safety and better IDE support than PropTypes. PropTypes checks are automatically stripped in production builds so they carry zero runtime cost for end users.
import PropTypes from 'prop-types';

function User({ name, age, email }) {
  return <p>{name} ({age}) — {email}</p>;
}

User.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number,
  email: PropTypes.string.isRequired
};
For modern projects, TypeScript interfaces are generally preferred over PropTypes for static type safety.

Why it matters: PropTypes provide runtime validation that warns you in development when a component receives wrong types. TypeScript does this at compile time. Both exist to catch bugs early from passing wrong data to components.

Real applications: Validating that a required userId was passed, ensuring an array prop is actually an array, checking that a callback prop is a function, and documenting the expected data shape of a component's props.

Common mistakes: Using PropTypes in a TypeScript project (redundant), not marking required props as .isRequired, and only validating at the top level of an object without using PropTypes.shape() to validate its structure.

State batching is React's built-in optimization of grouping multiple state updates that occur in the same synchronous block into a single re-render pass. Without batching, calling two setter functions inside an event handler would trigger two separate re-renders. React 18 introduced automatic batching everywhere — including inside setTimeout, Promise callbacks, and native event handlers — eliminating cases where updates were processed one at a time. This optimization dramatically reduces unnecessary renders and keeps the UI responsive during complex interactions.
function handleClick() {
  setCount(c => c + 1);   // batched
  setFlag(f => !f);        // batched
  setName("Alice");         // batched
  // React renders only ONCE after all three updates
}
React 18+ automatically batches all state updates, including those in promises, timeouts, and native event handlers. Earlier versions only batched inside React event handlers.

Why it matters: Batching prevents extra re-renders when multiple state changes happen together. Understanding this helps you predict when renders happen and why you don't see multiple renders for multiple sequential setState calls in one handler.

Real applications: Updating multiple pieces of state in a single user action (e.g., setting loading=false, data=result, error=null after an API call), and understanding why your component only renders once even after three setState calls.

Common mistakes: Not knowing batching was limited to React event handlers in React 17 and below (async callbacks caused multiple renders), using flushSync without understanding that it opts out of batching (forces immediate synchronous render), and expecting to read updated state right after calling setState (the update is queued, not applied immediately).

Derived state is any value that can be calculated directly from existing props or other state. Storing it as a separate state variable is almost always a mistake — it creates redundant data that can fall out of sync if one copy updates without the other. The better approach is to compute the value inline during render, or wrap it in useMemo if the computation is expensive. The rule of thumb: if a value can be derived from what you already have, don't store it in state — calculate it on the fly.
// Bad: duplicating state
const [items, setItems] = useState([]);
const [count, setCount] = useState(0); // derived from items.length

// Good: compute during render
const [items, setItems] = useState([]);
const count = items.length; // no extra state needed
Compute values during rendering when possible. Only use state for data that cannot be derived from other state or props.

Why it matters: Derived state creates two copies of truth that can get out of sync. If you calculate a value from existing state or props inside the render, React always shows the correct derived value with zero extra effort.

Real applications: Computing a cart total from an array of items (no need to store total separately), filtering a list based on a search term (compute the filtered list, don't store it), and generating display text from a status code.

Common mistakes: Storing derived values in state and manually syncing them with useEffect (fragile and error-prone), not realizing computation during render is cheap for most operations, and over-optimizing with useMemo before checking if it is actually needed.

When the new state value depends on the current state value, always use the functional updater form of the setter — pass a function that receives the current state and returns the next state. React batches multiple updates, so reading the state variable directly inside a handler can give you a stale snapshot from before other updates ran. The functional form setState(prev => prev + 1) always receives the most up-to-date accumulated state, making it safe in rapid updates, concurrent renders, and async callbacks.
// Risky — may read stale value when batched
setCount(count + 1);
setCount(count + 1); // both read same stale count!

// Safe — always gets the latest value
setCount(prev => prev + 1);
setCount(prev => prev + 1); // correctly increments twice

// Same rule for objects and arrays
setItems(prev => [...prev, newItem]);
setUser(prev => ({ ...prev, name: 'Bob' }));
Always use the functional form inside async callbacks, useEffect, or when calling the setter multiple times in one handler.

Why it matters: State updates are async and state values in a closure can be stale. The functional update form always receives the latest state, preventing bugs where multiple increments only apply once because they all read the same old value.

Real applications: A counter that increments multiple times in one click handler, updating a list by adding to the current items, and toggling a boolean value reliably regardless of when the setter runs.

Common mistakes: Using setCount(count + 1) multiple times in one handler and getting a single increment (all closures read the same stale value), not knowing the functional form exists, and using the functional form everywhere unnecessarily when the value doesn't depend on the previous state.

React enforces one-way (unidirectional) data flow — data always travels downward from parent to child via props, never upward or sideways directly. This constraint makes it easy to trace how data moves through your application: follow a prop back to the component that owns it. When a child needs to update the parent's state, the parent passes a callback function as a prop and the child calls it with the new value. This explicit, predictable flow is one of React's core architectural pillars and is what makes debugging manageable even in large applications.
function Parent() {
  const [selected, setSelected] = useState(null);
  return (
    <>
      <ItemList onSelect={setSelected} />
      <Detail item={selected} />
    </>
  );
}

function ItemList({ onSelect }) {
  return items.map(item =>
    <button key={item.id} onClick={() => onSelect(item)}>
      {item.name}
    </button>
  );
}
Unidirectional flow makes apps predictable and easy to debug — you always know where state lives and how it changes.

Why it matters: React's unidirectional data flow means state always flows from parent to child via props, and children communicate back through callbacks. This makes the data path explicit and traceable, unlike two-way binding where changes can happen in multiple places.

Real applications: Every React app follows this model. A parent holds the selected item state, passes it down to a list, and the list calls a parent-provided callback when an item is selected.

Common mistakes: Trying to pass data up via props (you can only pass down — use callbacks for upward communication), getting confused about why a child can't directly change a parent's state, and using two-way data binding patterns (like Angular's ngModel) conceptually when React requires explicit callbacks.

The lazy initializer pattern means passing a function (not a value) to useState — React calls it only once on the initial render and ignores it on every subsequent render. Without this, the expression inside useState() is evaluated on every render even though only the first call matters. This is especially important when the initial value comes from an expensive computation — like parsing a large JSON string, reading from localStorage, or processing a large dataset. Always use the lazy form useState(() => expensiveCalc()) when the initial value is costly to compute.
// Runs on EVERY render — slow if parsing is expensive
const [data, setData] = useState(JSON.parse(localStorage.getItem('data')));

// Lazy initializer — runs only ONCE on mount
const [data, setData] = useState(() => {
  const stored = localStorage.getItem('data');
  return stored ? JSON.parse(stored) : [];
});
Use the lazy initializer whenever the initial value requires computation, storage reads, or any work you don't want repeated on every render.

Why it matters: The function form of useState is only called once — on mount. If you pass a value directly, the expression is evaluated on every render even though it's only used the first time. For expensive operations, this difference matters.

Real applications: Reading from localStorage for initial state, parsing a complex initial data structure from props, generating a unique ID for the component on mount, and any expensive computation that produces the starting value.

Common mistakes: Writing useState(heavyCompute()) instead of useState(() => heavyCompute()) (the function runs on every render with the direct form), and not knowing this optimization exists at all.

Sibling components have no direct communication channel — they cannot read or write each other's state. The correct pattern is to lift the shared state up to their closest common ancestor, which then becomes the single source of truth. The parent holds the state, passes the current value down to both siblings as props, and passes a setter function to whichever sibling needs to trigger updates. This keeps all related data in one place and makes the data flow transparent and easy to trace.
function Parent() {
  const [search, setSearch] = useState('');
  return (
    <>
      {/* Sibling 1 — updates shared state */}
      <SearchInput value={search} onChange={setSearch} />
      {/* Sibling 2 — reads shared state */}
      <ResultList filter={search} />
    </>
  );
}
The parent becomes the single source of truth. For deeply nested sharing, use Context API or a state management library instead.

Why it matters: Sibling components cannot directly share state. Lifting state to the common parent is the clean, React-native solution, and understanding when to lift vs when to use Context is an important architectural decision.

Real applications: A tab bar and the content area that changes based on the selected tab, a sidebar filter and the main content list that responds to filter changes, and two components that both display and modify the same user object.

Common mistakes: Lifting state too high up the tree unnecessarily (causes unrelated components to re-render), not lifting state high enough (siblings go out of sync), and not knowing when to stop lifting and use Context or Zustand instead.

State is for values that should be visible in the UI — every state change triggers a re-render so the screen stays up to date. A ref persists values across renders without causing a re-render — use it for things like DOM node references, interval IDs, or caching the previous value of a state variable. The key rule: if the user needs to see it change on screen, use state; if it's purely internal bookkeeping that doesn't affect what the user sees, use a ref. Mixing them up leads to either missed renders (using ref when you should use state) or unnecessary re-renders (using state when you should use a ref).
// Use state — count appears in the UI
const [count, setCount] = useState(0);

// Use ref — timer ID is never displayed
const timerRef = useRef(null);
timerRef.current = setTimeout(callback, 1000);

// Use ref — track previous value without extra render
const prevValueRef = useRef(value);
useEffect(() => { prevValueRef.current = value; });
Rule of thumb: if the value needs to appear in rendered output, use useState. If it is purely internal, use useRef.

Why it matters: Both useState and useRef persist values across renders, but only useState causes a re-render when it changes. Choosing state for everything causes unnecessary re-renders; using refs for rendered values means the UI won't update.

Real applications: A timer ID stored in a ref (no need to show it), a previous value tracked in a ref for comparison, a scroll position used only for logic (not rendered), vs a counter that appears in the UI (must be state).

Common mistakes: Using state to store a timer ID or interval reference (triggers a re-render on set and clear, which is unnecessary), using a ref to store a value that should update the UI (user won't see the change), and not knowing you can store any mutable value in a ref, not just DOM elements.