Senior 6 min · March 05, 2026

React Props Direct Mutation — The Stale UI Bug

When a React child component refuses to update despite new data from parent, the culprit is often direct prop mutation.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • React components are functions returning JSX — the building blocks of any UI
  • Props are read-only data passed from parent to child, enabling reusability
  • Destructure props in the function signature for cleaner code; set defaults with default parameters
  • Passing new object/array literals as props breaks React.memo and triggers unnecessary re-renders
  • For parent-child communication, pass callback functions as props — data down, events up
  • The biggest mistake: mutating props directly instead of calling an updater function (causes stale UI)
Plain-English First

Think of a React component like a LEGO brick mould. The mould is always the same shape, but you can pour different coloured plastic in each time to get a different brick. Props are that coloured plastic — they're the custom data you pour into the same mould to get a unique result. A 'Button' component is one mould; the label, colour, and click action you pass in are the props that make each button unique across your whole app.

Every React app you've ever used — from a simple to-do list to a full dashboard — is built from components. They're not just a React quirk; they're the answer to one of the oldest problems in UI development: how do you build something complex without turning your codebase into an unmanageable wall of HTML and JavaScript? Components let you split a UI into independent, reusable pieces and reason about each one in isolation. That's not a small thing — it's the difference between a codebase that scales and one that collapses under its own weight six months in.

Props solve a specific follow-on problem: once you have reusable components, how do you make them dynamic? Without props, every component would be a hardcoded island. You'd need a separate 'WelcomeAlice' and 'WelcomeBob' component instead of one 'WelcomeUser' component that accepts a name. Props are the mechanism that lets parent components talk to their children — passing data down the tree so a single flexible component can serve dozens of use cases.

By the end of this article you'll understand not just how to write components and pass props, but WHY React enforces one-way data flow, when to reach for default props versus nullish coalescing, and how to avoid the three most common prop-related bugs that trip up developers who've been writing React for months. You'll also walk away with answers to the exact questions interviewers use to separate React users from React understanders.

What a React Component Actually Is (And Why It's a Function, Not a Class)

A React component is a JavaScript function that accepts an optional input object (props) and returns JSX — a description of what should appear on screen. That's it. The reason this is powerful is that React treats the return value as a blueprint, not a direct DOM instruction. React decides when and how to turn that blueprint into real DOM nodes, which is what enables features like batched updates, concurrent rendering, and the virtual DOM diff algorithm.

For years React had two component types: class components and function components. Class components required inheriting from React.Component, managing a this keyword, and writing lifecycle methods like componentDidMount. Function components were simpler but couldn't manage state — until Hooks arrived in React 16.8. Today, function components with hooks are the standard. You'll still encounter class components in legacy codebases, but every new component you write should be a function.

The function metaphor is worth taking seriously: a well-written component is a pure function. Same props in, same UI out. No surprises. This predictability is exactly why React apps are easier to test and debug than traditional imperative DOM manipulation — you always know what a component will render if you know its props and state.

UserProfileCard.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React from 'react';

// A function component — just a plain JavaScript function.
// React knows it's a component because it starts with a capital letter
// and returns JSX.
function UserProfileCard({ username, role, avatarUrl }) {
  // Destructuring props directly in the parameter list keeps the
  // function body clean. Equivalent to: const { username, role, avatarUrl } = props;

  return (
    <div className="profile-card">
      {/* Curly braces let us drop back into JavaScript inside JSX */}
      <img
        src={avatarUrl}
        alt={`Profile photo of ${username}`} // Template literals work fine inside JSX attributes
      />
      <h2>{username}</h2>
      {/* Conditional rendering — if role exists, show it; otherwise show nothing */}
      {role && <span className="role-badge">{role}</span>}
    </div>
  );
}

// Named export — preferred over default export for most components
// because it forces consistent naming across your codebase
export { UserProfileCard };

// --- How you'd use this in a parent component ---
// import { UserProfileCard } from './UserProfileCard';
//
// function App() {
//   return (
//     <UserProfileCard
//       username="Sarah Connor"
//       role="Admin"
//       avatarUrl="https://example.com/sarah.jpg"
//     />
//   );
// }
Output
Renders a card containing Sarah Connor's avatar image, an <h2> with 'Sarah Connor', and a role badge showing 'Admin'. If role were omitted, the badge simply wouldn't appear — no error, no 'undefined' text.
Why Capital Letters Matter:
React uses the capitalisation of the component name to decide how to handle JSX. Lowercase tags like <div> or <span> are treated as native HTML elements. Capital-letter tags like <UserProfileCard> tell React to look for a component function in scope and call it. This is not a style guide preference — it's a hard rule built into the JSX transform.
Production Insight
Renaming a component to start with lowercase after it's already in use causes React to treat it as an HTML tag.
Result: the component is never called — the app renders nothing where the component should be.
Rule: never change the casing of a component name after initial commit. Enforce naming conventions in lint.
Key Takeaway
Components are just functions returning JSX.
Same props in = same UI out. Pure functions are easy to test.
First letter capital = component; lower = HTML tag.

Props Are Read-Only — Here's Why That's a Feature, Not a Limitation

React enforces a strict rule: a component must never modify its own props. If you try it, React won't throw an immediate error, but you'll get unpredictable behaviour that's incredibly hard to debug. The reason for this rule is data flow clarity — in a React app, data flows in one direction: down from parent to child. Props represent the parent's decision about what to render. A child modifying its props would be like an employee rewriting their own job description without telling their manager — the system's source of truth becomes corrupted.

This one-way data flow is the architectural choice that makes React apps debuggable at scale. At any point you can trace exactly where a piece of data came from by walking up the component tree. If a button component is rendering the wrong label, you don't have to search the entire app — you look at what the parent passed down.

When a child component does need to communicate back up to a parent — say, a user clicked a button and the parent needs to know — you pass a callback function as a prop. The child calls the function; the parent handles the state change and re-renders with new props. Data goes down, events go up. Once this mental model clicks, React's architecture starts to feel elegant rather than restrictive.

QuantitySelector.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import React, { useState } from 'react';

// CHILD COMPONENT
// This component has no idea where quantity comes from or how
// onQuantityChange works — it just calls the function when needed.
// This is deliberate. It makes QuantitySelector reusable anywhere.
function QuantitySelector({ quantity, onQuantityChange, maxQuantity }) {
  return (
    <div className="quantity-selector">
      <button
        onClick={() => onQuantityChange(quantity - 1)} // Calls parent's handler — doesn't touch state directly
        disabled={quantity <= 1} // Props drive the UI state — no internal guessing
      >
        −
      </button>

      <span>{quantity}</span>

      <button
        onClick={() => onQuantityChange(quantity + 1)}
        disabled={quantity >= maxQuantity} // Parent dictates the ceiling via props
      >
        +
      </button>
    </div>
  );
}

// PARENT COMPONENT
// The parent owns the state. It passes data down as props,
// and a handler function the child can call to request a change.
function ProductPage() {
  const [selectedQuantity, setSelectedQuantity] = useState(1);

  // The parent decides what happens when quantity changes.
  // QuantitySelector doesn't need to know about this logic at all.
  function handleQuantityChange(newQuantity) {
    if (newQuantity >= 1 && newQuantity <= 10) {
      setSelectedQuantity(newQuantity);
    }
  }

  return (
    <div>
      <h1>Wireless Headphones</h1>
      <QuantitySelector
        quantity={selectedQuantity}        // Data flows DOWN as a prop
        onQuantityChange={handleQuantityChange} // Function flows DOWN so events can go UP
        maxQuantity={10}
      />
      <p>Total units selected: {selectedQuantity}</p>
    </div>
  );
}

export { ProductPage };
Output
Renders a product page with a quantity selector showing '1' with a disabled '−' button. Clicking '+' increments the count up to 10 (maxQuantity). The '-' button disables again at 1. The total units line updates in sync. If you try to click '+' at 10, nothing happens — the button is disabled.
Pro Tip — Name Handler Props Consistently:
The React community convention is to name event handler props with an 'on' prefix: onClick, onQuantityChange, onFormSubmit. Name the actual handler functions with 'handle': handleClick, handleQuantityChange. This pattern makes it immediately obvious which side of the parent/child boundary you're on when reading someone else's code.
Production Insight
A team spent two days debugging a stale UI issue.
Root cause: someone wrote props.user.name = newName inside the child component.
The mutation bypassed React's reconciliation — the parent's state and the UI were out of sync.
Rule: never mutate props. If you need to change data, call the parent's updater function.
Key Takeaway
Props are read-only.
Data flows down, events flow up.
If you find yourself mutating a prop, stop — the pattern is broken.
When a Child Needs to Change Data from Parent
IfChild only needs to display data, never change it
UsePass data as prop. No callback needed.
IfChild needs to notify parent of a user action
UsePass a callback function as prop. Child calls it with the new value.
IfChild needs to modify a deeply nested object
UsePass an updater function that returns a new object. Never mutate the object directly.

Default Props and PropTypes — Defensive Programming for Component APIs

Every component you export is an API. Other developers (or future you) will use it, and they will sometimes forget to pass a required prop. Default props and prop validation are your safety net.

Default prop values let your component degrade gracefully when a prop isn't provided. The modern way to set defaults is directly in the function signature using JavaScript's default parameter syntax — it's cleaner and doesn't require a separate defaultProps object. For TypeScript projects you'd use an interface or type to enforce the shape of props at compile time. For plain JavaScript projects, the 'prop-types' package gives you runtime warnings in development.

The value of PropTypes isn't catching production bugs — it's the warning you see in the console during development that says 'Hey, you passed a string where a number was expected.' That warning surfaces an integration mistake instantly, before it manifests as a cryptic UI bug three screens deep. Think of it as documentation that enforces itself.

A practical rule: add PropTypes to any component that will be used in more than one place in your app. For one-off layout components that only ever appear in a single parent, the overhead isn't worth it.

NotificationBanner.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import React from 'react';
import PropTypes from 'prop-types'; // npm install prop-types

// Mapping type strings to visual styles keeps the component
// flexible without requiring the parent to pass colour hex codes.
const BANNER_STYLES = {
  success: { backgroundColor: '#d4edda', borderColor: '#28a745', icon: '✓' },
  warning: { backgroundColor: '#fff3cd', borderColor: '#ffc107', icon: '⚠' },
  error:   { backgroundColor: '#f8d7da', borderColor: '#dc3545', icon: '✕' },
  info:    { backgroundColor: '#d1ecf1', borderColor: '#17a2b8', icon: 'ℹ' },
};

function NotificationBanner({
  message,
  type = 'info',       // Default value — if 'type' isn't passed, component stays functional
  isDismissible = true, // Defaults to true so most banners get a close button for free
  onDismiss,
}) {
  // Safely fall back to 'info' style if an unrecognised type is passed
  const bannerStyle = BANNER_STYLES[type] ?? BANNER_STYLES.info;

  return (
    <div
      style={{
        padding: '12px 16px',
        border: `1px solid ${bannerStyle.borderColor}`,
        backgroundColor: bannerStyle.backgroundColor,
        borderRadius: '4px',
        display: 'flex',
        alignItems: 'center',
        gap: '8px',
      }}
    >
      <span>{bannerStyle.icon}</span>
      <span style={{ flex: 1 }}>{message}</span>

      {/* Only render the dismiss button if both flags allow it */}
      {isDismissible && onDismiss && (
        <button onClick={onDismiss} aria-label="Dismiss notification">
          ×
        </button>
      )}
    </div>
  );
}

// PropTypes act as living documentation — any dev using this component
// can see exactly what it expects without reading the full source.
NotificationBanner.propTypes = {
  message: PropTypes.string.isRequired, // This one is mandatory — you must pass it
  type: PropTypes.oneOf(['success', 'warning', 'error', 'info']), // Constrained set of values
  isDismissible: PropTypes.bool,
  onDismiss: PropTypes.func, // Optional — only needed when isDismissible is true
};

export { NotificationBanner };

// --- Usage examples ---
// <NotificationBanner message="File saved successfully." type="success" />
// <NotificationBanner message="Session expiring in 5 minutes." type="warning" isDismissible={false} />
// <NotificationBanner
//   message="Payment failed. Please try again."
//   type="error"
//   onDismiss={() => setShowBanner(false)}
// />
Output
Three banners rendered:
1. Green banner with ✓ icon and 'File saved successfully.' — no dismiss button (onDismiss not passed)
2. Yellow banner with ⚠ icon, 'Session expiring in 5 minutes.' — no dismiss button (isDismissible=false)
3. Red banner with ✕ icon, 'Payment failed.' — × dismiss button present and clickable
Dev console (if message prop is missing): Warning: Failed prop type: The prop `message` is marked as required in `NotificationBanner`, but its value is `undefined`.
Watch Out — Default Props vs. defaultProps:
You may see older React code using Component.defaultProps = { type: 'info' } as a static property. This approach still works but is considered legacy. The React team has signalled that defaultProps for function components will be deprecated. Use default parameter values in the function signature instead — it's standard JavaScript and works identically.
Production Insight
PropTypes only warn in development mode — they are stripped from production builds.
A missing required prop in production silently renders undefined, often causing blank sections.
Use TypeScript for compile-time safety; PropTypes for developer documentation.
Key Takeaway
Default parameters in function signature for graceful fallbacks.
PropTypes for documentation and dev warnings.
TypeScript for production safety — catches mismatches before runtime.

The children Prop and Component Composition — React's Most Underused Superpower

Every React component automatically receives a special prop called children. It contains whatever JSX you nest between the component's opening and closing tags. This is how you build container components — components that handle layout, styling, or behaviour without caring about the specific content inside them.

Composition via children is often the right answer when developers instinctively reach for prop drilling or context. If you find yourself passing a chunk of JSX as a regular prop (like contentProp={<SomeComplexJSX />}), that's a signal to use children instead — it reads more naturally and mirrors how HTML itself works.

The real power emerges when you build generic wrappers: modals, cards, sidebars, permission guards, error boundaries, loading skeletons. None of these need to know what's inside them. They just provide a frame. This separation of concerns — the frame doesn't know about the content, the content doesn't know about the frame — is what makes large React codebases maintainable. Components become composable building blocks rather than tightly coupled units.

PermissionGuard.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import React from 'react';

// A 'guard' component that controls what the user can see
// based on their permissions. It knows nothing about the UI
// it's protecting — that's intentional.
function PermissionGuard({ requiredRole, currentUserRole, children, fallback }) {
  const ROLE_HIERARCHY = { viewer: 1, editor: 2, admin: 3 };

  const userLevel = ROLE_HIERARCHY[currentUserRole] ?? 0;
  const requiredLevel = ROLE_HIERARCHY[requiredRole] ?? Infinity;

  // If the user has sufficient permissions, render whatever was nested inside
  if (userLevel >= requiredLevel) {
    return <>{children}</>; // The <></> is a React Fragment — a wrapper with no DOM output
  }

  // Otherwise render the fallback, or nothing if none was provided
  return fallback ?? null;
}

// A simple Card wrapper — pure composition, pure layout concern
function DashboardCard({ title, children }) {
  return (
    <div className="dashboard-card" style={{ border: '1px solid #e0e0e0', borderRadius: '8px', padding: '20px', margin: '12px 0' }}>
      <h3 style={{ marginTop: 0 }}>{title}</h3>
      {/* children renders whatever the parent put between <DashboardCard> tags */}
      {children}
    </div>
  );
}

// --- How these compose together in a real dashboard ---
function AdminDashboard({ loggedInUser }) {
  return (
    <div>
      <DashboardCard title="Overview">
        {/* This content is 'children' of DashboardCard */}
        <p>Welcome back, {loggedInUser.name}.</p>
        <p>Last login: {loggedInUser.lastLogin}</p>
      </DashboardCard>

      <PermissionGuard
        requiredRole="admin"
        currentUserRole={loggedInUser.role}
        fallback={<p style={{ color: '#999' }}>You don't have access to user management.</p>}
      >
        {/* This entire block only renders if the user is an admin */}
        <DashboardCard title="User Management">
          <button>Invite New User</button>
          <button style={{ marginLeft: '8px' }}>Manage Roles</button>
        </DashboardCard>
      </PermissionGuard>
    </div>
  );
}

export { AdminDashboard, DashboardCard, PermissionGuard };

// loggedInUser = { name: 'Jordan', lastLogin: '2024-01-15', role: 'editor' }
// Result: Overview card renders. User Management card is REPLACED by the fallback message.
//
// loggedInUser = { name: 'Sam', lastLogin: '2024-01-14', role: 'admin' }
// Result: Both cards render. Admin sees the Invite and Manage buttons.
Output
For 'editor' user Jordan:
- DashboardCard 'Overview' renders with welcome text and last login date
- 'User Management' section shows: 'You don't have access to user management.' in grey text
For 'admin' user Sam:
- DashboardCard 'Overview' renders with welcome text
- DashboardCard 'User Management' renders with 'Invite New User' and 'Manage Roles' buttons
Interview Gold — Composition vs. Inheritance:
React's docs explicitly recommend composition over inheritance. If an interviewer asks 'how do you share logic between components?', the answer isn't 'extend a base component class'. It's 'extract the logic into a custom hook or a wrapper component and compose them'. The PermissionGuard above is a real-world example of this pattern — it encapsulates access-control logic without any component needing to inherit from a base class.
Production Insight
A common production crash: {children.map(...)} when children is undefined (self-closing tag).
The error 'Cannot read properties of undefined (reading map)' is a React rookie mistake.
Fix: use React.Children.map(children, fn) — it safely handles all child types including undefined.
Key Takeaway
children prop enables generic container components.
Use it to separate layout from content.
Always use React.Children.map if you need to iterate — never assume children is an array.

Props vs State: The Fundamental Distinction That Defines Data Ownership

One of the most common sources of confusion for React beginners is knowing when to use props versus state. The rule is simple but profound: props are for data the parent owns and passes down; state is for data the component owns and manages itself. If a value can be computed from existing props or state, it should be neither — compute it during render.

Think of state as private memory for a component. Click a button to open a modal? That's state — const [isOpen, setIsOpen] = useState(false). The parent doesn't care whether the modal is open or closed. But if the parent needs to know the modal's state — say, to disable a button while the modal is open — then that value should live in the parent and be passed down as a prop.

A common anti-pattern is duplicating props into state. You'll see code like const [name, setName] = useState(props.name). This breaks the single source of truth: if the parent later passes a different name, the local state won't update because useState only reads the initial value once. If you need to sync, either compute from the prop directly or use useEffect with the prop in the dependency array (but that's often a code smell). The better approach is derived state: compute from props without storing in state.

DerivedState.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React, { useState, useEffect } from 'react';

// BAD: duplicating props into state
function BadCounter({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  // If parent changes initialCount after first render, count never updates
  return <div onClick={() => setCount(c => c + 1)}>{count}</div>;
}

// GOOD: use prop as initial value and ignore future changes (if that's the intent)
function GoodCounter({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  // Only uses initialCount for initialisation. Parent's later changes ignored.
  return <div onClick={() => setCount(c => c + 1)}>{count}</div>;
}

// BETTER: if you need to sync when prop changes, use derived state via useMemo or direct computation
function SyncedDisplay({ count }) {
  // No state needed — compute display text directly from prop
  const displayText = `Current count: ${count}`;
  return <div>{displayText}</div>;
}

// Use case: a full name derived from first and last
function FullNameDisplay({ firstName, lastName }) {
  // Derived state — no useState needed
  const fullName = `${firstName} ${lastName}`.trim();
  return <p>{fullName}</p>;
}
Output
BadCounter: if parent passes a different initialCount after first render, the display doesn't change.
GoodCounter: same behaviour — initialCount is only used once, which is fine if the component is designed to be uncontrolled.
SyncedDisplay: always shows the latest count because it reads directly from props.
FullNameDisplay: concatenates first and last without any state.
Mental Model: Props Are Arguments, State Is Memory
  • A pure function has no state — it just transforms arguments to output.
  • State is for ephemeral data the component creates and manages.
  • If two components need to share the same piece of data, lift it to a common parent and pass it down as props.
  • Duplicating props into state breaks the single source of truth.
Production Insight
A bug where a form's initial values came from props but didn't update when the parent changed the prop.
Root cause: const [formData, setFormData] = useState(props.initialData) — initialises once, never re-syncs.
Fix: either make the form fully controlled by passing values and handlers, or fully uncontrolled and use key to remount on changes.
Key Takeaway
Props come from outside; state lives inside.
Don't duplicate props into state.
If it can be computed from props, compute it — don't store it.
● Production incidentPOST-MORTEMseverity: high

The Silent Mutation: Directly Changing Props Caused a Stale UI Bug

Symptom
After updating a user's name via a form, the profile card occasionally showed the old name until a full page refresh. Console logs showed the parent had the new name, but the child still rendered the old one.
Assumption
The team assumed the issue was a race condition in the API call. They wrapped the fetch in useEffect with proper dependency array, but the problem persisted.
Root cause
Inside the child component (UserProfileCard), someone had written props.user.name = newName directly instead of calling the parent's onUserUpdate callback. This mutated the prop's object reference in place, so React's shallow comparison detected no difference in the prop reference and skipped re-render.
Fix
Removed the mutation and used the callback: onUserUpdate({ ...user, name: newName }). Also added a PropTypes warning for required onUserUpdate to prevent similar issues in future.
Key lesson
  • Never mutate props directly — ever. Treat props as immutable reads.
  • If a child needs to change data, the parent must own the state and provide an updater function.
  • Use PropTypes or TypeScript to enforce that updater functions are passed.
Production debug guideSymptoms and actions for the three most frequent prop-related bugs in production React apps.4 entries
Symptom · 01
Child component doesn't re-render when parent state updates
Fix
Check if parent is passing a new reference. Use useMemo for objects/arrays. Verify React.memo is not incorrectly blocking re-render due to stale reference equality.
Symptom · 02
Prop type warning in console: 'Failed prop type: The prop x is marked as required'
Fix
Add default value for optional props in the function signature. Ensure parent component passes the required prop. Use TypeScript for compile-time enforcement instead of just PropTypes.
Symptom · 03
children.map is not a function runtime error
Fix
Use React.Children.map(children, fn) which safely handles single child, array, or undefined. Alternatively, coerce with React.Children.toArray(children).map(...).
Symptom · 04
React.memo doesn't prevent re-renders despite identical props
Fix
Check for inline object/function literals passed as props. Each render creates a new reference. Move them outside the component or wrap with useMemo/useCallback.
Function Components vs Class Components
AspectFunction ComponentClass Component
SyntaxPlain JS function — concise and familiarES6 class extending React.Component — verbose
State managementuseState / useReducer hooksthis.state and this.setState()
Side effectsuseEffect hookcomponentDidMount, componentDidUpdate, componentWillUnmount
Props accessDirect via function parameter or destructuringVia this.props — 'this' binding issues are common
BoilerplateMinimal — just a function and a returnConstructor, render method, lifecycle methods
Performance optimisationReact.memo() wraps the whole componentshouldComponentUpdate() or PureComponent
Current recommendation✅ Preferred for all new code⚠ Legacy — maintain only, don't write new ones
Hook supportFull support for all hooksNo hook support — class-only APIs instead

Key takeaways

1
Props flow strictly downward
parent to child. If a child needs to change something in the parent, you pass a callback function as a prop and call it. Data down, events up. This is not a limitation; it's what makes React apps debuggable.
2
Destructure props in the function signature and set defaults there too
function Card({ title, variant = 'default' }) is cleaner than accessing props.title throughout and setting Card.defaultProps separately.
3
The children prop turns any component into a reusable container. Before you create a new specialised component, ask whether a generic wrapper with children would serve the same purpose with less coupling.
4
PropTypes aren't about catching runtime crashes
they're about surfacing integration mistakes in development before they become mysterious UI bugs. Add them to any component used in more than one place.
5
Don't duplicate props into state. If a value can be computed from props or state, compute it during render. Duplication leads to stale UI and bugs.

Common mistakes to avoid

4 patterns
×

Mutating props directly inside a child component

Symptom
The UI shows stale data or behaves unpredictably; the parent's state is out of sync. Changes appear to work but React doesn't re-render properly.
Fix
Never mutate props. Instead, call an updater function passed as a prop from the parent. For objects, always create a new reference when making changes.
×

Passing inline object/array literals as props, breaking React.memo and causing unnecessary re-renders

Symptom
A child component wrapped in React.memo re-renders on every parent render even though the values haven't changed. Perceived performance degradation.
Fix
Define objects/arrays outside the component or wrap with useMemo/useCallback to maintain stable references between renders.
×

Assuming children is always an array and calling .map() directly

Symptom
Runtime error: 'Cannot read properties of undefined (reading map)' or 'children.map is not a function' when the component is self-closing or receives a single child.
Fix
Use React.Children.map(children, fn) instead of children.map(). React.Children handles all child types safely. Alternatively, call React.Children.toArray(children).map(...) if you need a true array.
×

Using defaultProps static property on function components

Symptom
Works but is deprecated. The React team has signalled that defaultProps for function components will be removed in a future release. Newbies may not know the modern alternative.
Fix
Use default parameter values in the function signature: function Card({ title, variant = 'default' }) { ... }
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What does 'one-way data flow' mean in React, and why is it important? Ca...
Q02SENIOR
What's the difference between a component's props and its state? If a pi...
Q03SENIOR
If you have a deeply nested component that needs a value from a top-leve...
Q01 of 03SENIOR

What does 'one-way data flow' mean in React, and why is it important? Can you give an example of how a child component communicates back to its parent without violating this principle?

ANSWER
One-way data flow means data moves only from parent to child through props. The parent owns the data and passes it down; the child cannot modify it directly. This is important because it makes the data flow predictable and debuggable — at any point you can trace where a value came from by walking up the component tree. To communicate back, the child receives a callback function as a prop. When an event occurs (e.g., a button click), the child calls that function with the new data. The parent then updates its state, and React re-renders with fresh props flowing down. Example: a QuantitySelector component calls onQuantityChange(quantity + 1) instead of setting its own quantity. This keeps the source of truth in the parent.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Can a React component change its own props?
02
What's the difference between props and state in React?
03
What is prop drilling and when does it become a problem?
04
What is the difference between `defaultProps` and default parameter values in function components?
🔥

That's React.js. Mark it forged?

6 min read · try the examples if you haven't

Previous
Introduction to React
2 / 47 · React.js
Next
React State Management