DeepFrontend
Practice ArenaBlogSign in
Go Pro
DeepFrontend

Interview-grade learning paths and hands-on practice for mid-to-senior engineers. Master internals, patterns, and system architecture with runnable code.

All systems operational

Core Courses

  • -JavaScript Internals
  • -React Reconciliation
  • -TypeScript rigor
  • -Next.js Caching
  • -Node.js Event Loop
  • -System Design
  • -Web Security
  • -CSS & Page Layouts

Explore

  • Learning Paths
  • Practice Arena
  • Pricing Options
  • HTML Sitemap

Resources

  • Privacy Policy
  • Terms of Service
  • Email Support

© 2026 DeepFrontend. All rights reserved.

Expert learning environments for web engineering teams.

Home/React/Props
beginner15 min read·Updated Jun 2025

Props

Props are just a function's parameters — one plain object, always read-only. Almost every 'why isn't my UI updating' bug for a beginner traces back to mutating that object instead of asking the parent for a new one.

propsimmutabilitychildrenprop-drillingfundamentals

Knowledge Check

4 questions · pass at 70%

0/4
easymcq

1. What is `props`, technically?


Interview Questions

2 questions

Cheatsheet

Download-ready reference
Cheatsheet
props = ONE plain object = the single argument React passes to your component fn.
  <Foo a={1} b="x">child</Foo>  →  Foo({ a: 1, b: "x", children: "child" })

Props are ALWAYS read-only. NEVER mutate props.x or a destructured prop —
  compute a new value / lift the change up to the parent's state instead.

children = whatever is nested between a component's tags — just a normal prop.

Destructuring defaults trigger ONLY on undefined:
  fn({})                → default applies
  fn({ x: undefined })  → default applies
  fn({ x: null })       → default does NOT apply (null is a real value)

Perf trap: {color:"red"} / () => {} written INLINE in JSX = a NEW reference
  every render → defeats React.memo. Stabilize with useMemo/useCallback
  only where a memoized consumer actually needs it.

Related topics

ComponentsJSX

On this page

15m
Knowledge CheckInterview QuestionsCheatsheet

Concept#

The beginner framing: props ("properties") are how a parent component passes data down into a child — like arguments passed into a function.

The precise mental model: props are a function's argument, literally — a component is a function (see Components), and React calls it with exactly one argument: a single plain object containing every attribute you wrote on the JSX tag, plus a children key for anything nested inside it.

<Greeting name="Ada" enthusiastic />
// React calls: Greeting({ name: "Ada", enthusiastic: true })
 
function Greeting({ name, enthusiastic }) {
  return <h1>Hello, {name}{enthusiastic ? "!" : "."}</h1>;
}

enthusiastic with no value becomes true — JSX treats a bare attribute name as shorthand for attr={true}, mirroring how HTML boolean attributes (disabled, checked) work.

Props are read-only — always#

function Counter({ count }) {
  count = count + 1; // ❌ mutating the parameter — React never sees this, and it's a real anti-pattern
  return <div>{count}</div>;
}

A component must never modify its own props. This isn't a style guideline — it's a hard rule, because the same props object might be read again elsewhere, or compared by reference later (see Rendering Lifecycle for exactly how React.memo relies on props not being mutated in place). If a value needs to change over time, it belongs in state (State), not as a mutated prop — the child instead calls a function passed down as a prop, asking the parent to compute and pass a new value.

children: the prop every nested JSX content becomes#

function Card({ children }) {
  return <div className="card">{children}</div>;
}
 
<Card>
  <h2>Title</h2>
  <p>Body text.</p>
</Card>
// Card receives: { children: [<h2>Title</h2>, <p>Body text.</p>] }

Anything written between a component's opening and closing tags is passed as the children prop automatically — there's nothing special about the name beyond convention; it's populated the exact same way any other JSX attribute would be.

Default values and destructuring#

function Button({ label, variant = "primary" }) { // default applies when the prop is UNDEFINED
  return <button className={variant}>{label}</button>;
}
 
<Button label="Save" />              // variant defaults to "primary"
<Button label="Save"

Destructuring defaults trigger specifically on undefined, not on any other falsy value — null, 0, and "" are all considered "a value was explicitly provided" and skip the default.


Try It#

Predict what logs before checking the solution.

function Profile(props) {
  props.name = props.name.toUpperCase(); // seems harmless...
  return <h2>{props.name}</h2>;
}
 
const data = { name: "ada" };
console.log(<Profile {...data} />); // renders once
Solution

If data were passed by reference and mutated directly, data.name would log "ADA" — but React's props objects are spread/copied per render internally in many cases, so the actual behavior here is subtle and version/implementation-dependent, which is exactly the point: mutating props is unpredictable across renders and React versions, entirely aside from whether it "seems to work" in one specific case. The safe, correct version never assigns to props.x at all:

function Profile({ name }) {
  return <h2>{name.toUpperCase()}</h2>; // compute a NEW value, never mutate the prop itself
}

Implement It Yourself#

Build a withDefaults helper that mimics destructuring defaults manually — useful for internalizing exactly when a default does and doesn't apply:

function withDefaults(props, defaults) {
  const result = { ...props };
  for (const key in defaults) {
    if (result[key] === undefined) { // mirrors destructuring defaults EXACTLY: only undefined triggers it
      result[key] = defaults[key];
    }
  }
  return result;
}
 
withDefaults({ label: 



This is precisely what function Button({ variant = "primary" }) does under the hood — the = default in a destructuring pattern only ever fires when the property is missing or explicitly undefined.


Under the Hood#

Props being "just an argument" is why props are immutable by convention rather than by language-level enforcement (JavaScript doesn't freeze the object) — the same discipline that Functional Programming's purity principle asks for: treat inputs as read-only, and produce new values instead of mutating in place. A component that mutates its props is doing exactly the "impure function that mutates its argument" anti-pattern from that topic, just wearing React's clothing.


Common Mistakes#

1. Mutating a prop directly#

Covered above — never assign to props.x or to a destructured prop variable. Compute a new value instead.

2. Passing a new object/array/function literal as a prop on every render#

function Parent() {
  return <Child style={{ color: "red" }} onClick={() => doThing()} />; // ❌ NEW object + function every render
}

{ color: "red" } and () => doThing() are freshly created every time Parent renders — even though they look identical, they're different references each time. If Child is wrapped in React.memo expecting stable props to skip re-rendering, this defeats that optimization completely, since the props are "different" by reference every time (see Rendering Lifecycle).

3. Deep-cloning props "just to be safe"#

function Widget({ config }) {
  const safeCopy = JSON.parse(JSON.stringify(config)); // usually unnecessary
  ...
}

Since you should never be mutating props anyway, defensively deep-cloning them is almost always unneeded overhead — the discipline of "don't mutate" makes the copy pointless. (It also silently breaks anything in config that isn't JSON-serializable, like functions or Date objects.)


Best Practices#

  • Treat every prop as read-only, full stop — if something needs to change, it's state in the parent, updated via a callback prop.
  • Use destructuring in the function signature (function Button({ label, variant })) rather than accessing props.label repeatedly — clearer, and makes required vs. defaulted props visible at a glance.
  • Keep prop lists short and focused — a component needing 15 props is often a sign it's doing too much (see Components's composition guidance).
  • Type your props (TypeScript, or at minimum clear naming/documentation) so misuse is caught before runtime.

Performance Tips#

  • Passing inline object/array/function literals as props (Common Mistake #2) is one of the most common things that silently defeats React.memo — if a child is memoized specifically to avoid expensive re-renders, audit every prop the parent passes for "is this the same reference every time, or a fresh literal?"
  • useMemo/useCallback exist specifically to give an inline object/function a stable reference across renders when needed — but reach for them only where a memoized child (or a dependency array elsewhere) actually depends on that stability, not as a blanket default on every prop.

variant
=
{
undefined
}
/>
// ALSO defaults — undefined triggers the default
<Button label="Save" variant={null} /> // does NOT default — null is a real, passed value
console.log(data.name); // what does THIS log?
"Save"
}, { variant:
"primary"
});
// { label: "Save", variant: "primary" }
withDefaults({ label: "Save", variant: null }, { variant: "primary" });
// { label: "Save", variant: null } — null is a real value, NOT replaced