In my first React job, I shipped a bug that crashed our entire user dashboard. The culprit? I was treating props like state and accidentally mutating them directly. That painful lesson taught me something no tutorial ever did: understanding props vs state isn't just theory—it's the difference between maintainable code and production disasters.
The question every React developer faces: "Should this be props or state?"
If you've been wrestling with this decision, you're in good company. After building 40+ React applications and mentoring dozens of developers, I've learned that mastering this distinction is what separates junior developers from those who write production-ready code.
- Why your component architecture matters more than you think
- The mental model that changed how I approach component design
- Real patterns I use to make these decisions in seconds
Let's break this down with the context you need to build React apps that scale.
The Mental Model That Changed Everything
Props: Your Component's API Contract
- The interface between parent and child components
- Think of them as function parameters for your component
- Immutable by design—changing them breaks React's rendering model
- They define what your component can do, not what it remembers
"Props are the arguments you pass to configure behavior. State is the memory that drives that behavior."
State: Your Component's Private Memory
- Internal data that can change over time
- Managed through
useState()oruseReducer() - Triggers re-renders when updated
- Should be minimal and specific to avoid performance issues
"State answers: What does this component need to remember between renders?"
A Real-World Scenario
Picture this: You're building a user profile card for a social media app. The card needs to display user information (name, avatar, bio) and allow users to follow/unfollow.
The user data comes from props—it's passed down from a parent component that fetched it from an API. You can't and shouldn't modify this data directly.
The follow status lives in state—it's specific to this user's interaction with this profile and needs to update when they click the follow button.
This separation keeps your components predictable and your data flow clear.
Real React Code Example (Props vs State)
👇 Component with Internal State
import { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0); // component's private memory
return (
<div>
<h2>Count: {count}</h2>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
Here, count is internal to the component. Each Counter maintains its own count value. When you click the button, only this component re-renders—that's the power of localized state.
👇 Component Configured via Props
const Welcome = ({ name, role, onUserClick }) => {
return (
<div onClick={() => onUserClick(name)}>
<h1>Hello, {name}!</h1>
<p>Role: {role}</p>
</div>
);
};
const App = () => {
const handleUserClick = (userName) => {
console.log(`${userName} was clicked`);
};
return (
<Welcome
name="Sarah"
role="Developer"
onUserClick={handleUserClick}
/>
);
};
The Welcome component is completely configured by its props. It's reusable—pass different props, get different behavior. Notice how we also pass functions as props to handle events.
⚔️ Detailed Comparison Table — Props vs State
| Feature | Props | State |
|---|---|---|
| Source | Passed from parent | Defined and managed inside component |
| Editable? | ❌ Read-only | ✅ Can change using setState |
| Purpose | Configure component behavior | Track component's internal state |
| Triggers re-render? | ✅ Yes, if value changes | ✅ Yes, always on change |
| Used by | All React components | Components needing internal memory |
| Lifecycle role | Configuration input | Dynamic memory |
| Who updates it? | Parent component | The component itself |
My Decision Framework for Props
After years of React development, I use this mental checklist:
- Does this data come from outside the component? → Props
- Do I want this component to be reusable? → Props for configuration
- Should the parent control this behavior? → Props
- Is this data the same for the component's entire lifecycle? → Props
Props create predictable, testable components. When I can configure a component entirely through props, I know it's going to be easy to debug and maintain.
My Decision Framework for State
State is for when props aren't enough:
- Does this change based on user interaction? → State
- Do I need to track: form inputs, loading status, modal visibility, search filters? → State
- Does this component need to "remember" something between renders? → State
- Is this specific to this component instance? → State
I keep state as minimal as possible. Each piece of state is a potential source of bugs, so I only add it when I truly need dynamic behavior.
Production Pattern: Combining Props and State
Here's a pattern I use constantly—a component that's configurable via props but manages its own interaction state:
const LikeButton = ({
postId,
initialLiked = false,
onLike,
disabled = false
}) => {
const [liked, setLiked] = useState(initialLiked);
const [loading, setLoading] = useState(false);
const handleLike = async () => {
if (disabled || loading) return;
setLoading(true);
try {
await onLike(postId, !liked);
setLiked(!liked);
} catch (error) {
// Handle error - maybe show a toast
console.error('Failed to like post:', error);
} finally {
setLoading(false);
}
};
return (
<button
onClick={handleLike}
disabled={disabled || loading}
className={`btn ${liked ? 'liked' : 'not-liked'}`}
>
{loading ? '...' : liked ? "❤️" : "🤍"} Like
</button>
);
};
Usage in a parent component:
const PostCard = ({ post }) => {
const handleLike = async (postId, isLiked) => {
// API call to like/unlike post
await api.updateLike(postId, isLiked);
};
return (
<div className="post-card">
<h3>{post.title}</h3>
<p>{post.content}</p>
<LikeButton
postId={post.id}
initialLiked={post.isLiked}
onLike={handleLike}
/>
</div>
);
};
This pattern gives you the best of both worlds: reusable component logic with customizable behavior. The parent controls what happens when you like a post, but the button manages its own visual state and loading behavior.
Hard-Learned Lessons About Props and State
- ✅ Never mutate props directly — I've seen this break React's reconciliation and cause silent bugs
- ✅ Minimize state surface area — fewer state variables mean fewer ways things can go wrong
- ✅ Use multiple
useStatecalls rather than one giant state object - ✅ Lift state up sparingly — only when multiple components truly need shared access
- ✅ Consider derived state — sometimes you don't need state at all, just computed values
- ⚠️ Watch for stale closures — a common gotcha with state in event handlers
✅ Final Summary – Props vs State in React
| Category | Props | State |
|---|---|---|
| What it is | Component configuration API | Component's private memory |
| Mutable? | ❌ No | ✅ Yes |
| Data flow | Parent → Child | Internal to component |
| React Hook Needed? | ❌ No | ✅ useState() or useReducer() |
| Primary purpose | Reusability and configuration | Dynamic behavior and memory |
| Performance impact | Re-render when props change | Re-render when state changes |
Questions I Get About Props vs State in React
1. Can I update props inside a component?
No, and for good reason. Props are React's contract that data flows in one direction. Mutating props breaks this contract and can cause unpredictable re-render behavior. Always update props from the parent component.
2. Can I pass state as a prop to a child?
Absolutely—this is a fundamental React pattern. You often pass both state values and state setters as props to create controlled components. This keeps your data flow predictable while allowing child components to trigger updates.
3. When should I "lift state up"?
When multiple sibling components need access to the same data. Move the state to their closest common parent and pass it down as props. But don't over-do it—lifting state too high creates unnecessary re-renders and prop drilling.
4. Can a component have both props and state?
Yes, and most production components do. Props configure how the component behaves, state tracks how it's currently behaving. This separation keeps your components both reusable and interactive.
5. What about derived state vs props?
If you can compute a value from existing props or state, don't store it in state. Use useMemo for expensive calculations. Derived state is often a sign that you can simplify your component logic.