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.
- 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)
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.
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.
props.user.name = newName inside the child component.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.
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.
{children.map(...)} when children is undefined (self-closing tag).React.Children.map(children, fn) — it safely handles all child types including undefined.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.
- 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.
const [formData, setFormData] = useState(props.initialData) — initialises once, never re-syncs.key to remount on changes.The Silent Mutation: Directly Changing Props Caused a Stale UI Bug
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.onUserUpdate({ ...user, name: newName }). Also added a PropTypes warning for required onUserUpdate to prevent similar issues in future.- 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.
useMemo for objects/arrays. Verify React.memo is not incorrectly blocking re-render due to stale reference equality.x is marked as required'children.map is not a function runtime errorReact.Children.map(children, fn) which safely handles single child, array, or undefined. Alternatively, coerce with React.Children.toArray(children).map(...).useMemo/useCallback.Key takeaways
function Card({ title, variant = 'default' }) is cleaner than accessing props.title throughout and setting Card.defaultProps separately.Common mistakes to avoid
4 patternsMutating props directly inside a child component
Passing inline object/array literals as props, breaking React.memo and causing unnecessary re-renders
Assuming children is always an array and calling .map() directly
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
Interview Questions on This Topic
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?
onQuantityChange(quantity + 1) instead of setting its own quantity. This keeps the source of truth in the parent.Frequently Asked Questions
That's React.js. Mark it forged?
6 min read · try the examples if you haven't