Props (properties) are data passed from a parent component to a child component in React. They are immutable by design to ensure predictable data flow and prevent side effects.
✅ Why props are immutable:
❌ What you can’t do with props:
props.name = 'new name'
)Key rule: A component can read props but cannot change them - the parent component is responsible for that.
Props (short for properties) are one of the fundamental concepts in React, representing an object with data passed from a parent component to a child. Understanding props immutability is critically important for proper work with React applications.
Props are a way to pass data from parent to child components:
// Parent component passes data
function App() {
return (
<div>
<UserProfile
name="Alexander"
age={25}
hobbies={['programming', 'music']}
/>
<UserSettings theme="dark" notifications={true} />
</div>
);
}
// Child component receives props
function UserProfile(props) {
return (
<div>
<h1>{props.name}</h1>
<p>Age: {props.age}</p>
<ul>
{props.hobbies.map(hobby => <li key={hobby}>{hobby}</li>)}
</ul>
</div>
);
}
// With destructuring
function UserSettings({ theme, notifications }) {
return (
<div>
<p>Theme: {theme}</p>
<p>Notifications: {notifications ? 'on' : 'off'}</p>
</div>
);
}
React uses unidirectional data flow, where data flows from top to bottom:
// ❌ Incorrect - attempting to modify props
function ChildComponent(props) {
// props.name = 'New name'; // ❌ Error! Props cannot be changed
return <div>Hello, {props.name}!</div>;
}
// ✅ Correct - component simply uses props
function ChildComponent({ name }) {
return <div>Hello, {name}!</div>;
}
// If you need to change data, use state
function ParentComponent() {
const [name, setName] = useState('Alexander');
return (
<div>
<ChildComponent name={name} />
<button onClick={() => setName('Anna')}>
Change name
</button>
</div>
);
}
Props immutability prevents side effects and unpredictable behavior:
// ❌ Dangerous code - modifying props can affect other components
function BadComponent(props) {
// props.user.name = 'New name'; // ❌ This can affect the parent component!
return <div>{props.user.name}</div>;
}
// ✅ Safe code - creating a copy for changes
function GoodComponent({ user }) {
// Create a copy for safe changes
const modifiedUser = { ...user, name: 'New name' };
return <div>{modifiedUser.name}</div>;
}
Immutability simplifies debugging and understanding data flow:
// Easy to track where data came from
function UserCard({ user, onEdit }) {
// We definitely know that user came from above and won't change inside the component
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<button onClick={() => onEdit(user)}>
Edit
</button>
</div>
);
}
React uses reference comparison for rendering optimization:
// React can efficiently compare props
function ExpensiveComponent({ data }) {
// If the data reference hasn't changed, the component won't re-render
const processedData = useMemo(() => {
return data.map(item => processItem(item));
}, [data]);
return (
<div>
{processedData.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
// If props were mutable, React couldn't rely on reference comparison
Components can freely read props:
// Functional component
function WelcomeMessage({ name, greeting = 'Hello' }) {
return <h1>{greeting}, {name}!</h1>;
}
// With destructuring and default values
function UserCard({
user: { name, email, avatar },
showAvatar = true,
onEdit = () => {}
}) {
return (
<div className="user-card">
{showAvatar && <img src={avatar} alt={name} />}
<h2>{name}</h2>
<p>{email}</p>
<button onClick={onEdit}>Edit</button>
</div>
);
}
Parent components pass data through attributes:
function App() {
const userData = {
name: 'Alexander',
email: 'alex@example.com',
avatar: '/avatar.jpg'
};
const handleEdit = (user) => {
console.log('Editing user:', user);
};
return (
<div>
{/* Passing individual props */}
<WelcomeMessage name="Anna" greeting="Hello" />
{/* Passing object as spread */}
<UserCard
user={userData}
showAvatar={true}
onEdit={handleEdit}
/>
{/* Using spread operator */}
<UserProfile {...userData} />
</div>
);
}
When you need to change data from props, use state:
// ❌ Incorrect
function Counter({ initialValue }) {
let count = initialValue; // ❌ Local variable won't update
return (
<div>
<p>Counter: {count}</p>
<button onClick={() => count++}> {/* ❌ Doesn't work */}
Increase
</button>
</div>
);
}
// ✅ Correct
function Counter({ initialValue }) {
const [count, setCount] = useState(initialValue);
return (
<div>
<p>Counter: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increase
</button>
</div>
);
}
// ❌ Incorrect - mutating object from props
function TodoItem({ todo, onUpdate }) {
const toggleComplete = () => {
// todo.completed = !todo.completed; // ❌ Don't mutate props!
onUpdate(todo);
};
return (
<div className={todo.completed ? 'completed' : ''}>
<span>{todo.text}</span>
<button onClick={toggleComplete}>
{todo.completed ? 'Undo' : 'Complete'}
</button>
</div>
);
}
// ✅ Correct - creating a new object
function TodoItem({ todo, onUpdate }) {
const toggleComplete = () => {
// Create a new object with updated value
const updatedTodo = { ...todo, completed: !todo.completed };
onUpdate(updatedTodo);
};
return (
<div className={todo.completed ? 'completed' : ''}>
<span>{todo.text}</span>
<button onClick={toggleComplete}>
{todo.completed ? 'Undo' : 'Complete'}
</button>
</div>
);
}
A special type of props - children, which contains the component’s content:
// Component that wraps content
function Card({ title, children }) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-content">
{children} {/* Content passed between tags */}
</div>
</div>
);
}
// Usage
function App() {
return (
<Card title="My Profile">
<p>This is card content</p>
<button>Action</button>
</Card>
);
}
Functions are often passed as props for callbacks:
// Button component with customizable handler
function CustomButton({ onClick, children, variant = 'primary' }) {
// Never call onClick directly in the component body!
// onClick(); // ❌ This would call the function on every render
return (
<button
className={`btn btn-${variant}`}
onClick={onClick} // ✅ Pass function as event handler
>
{children}
</button>
);
}
// Usage
function App() {
const handleClick = () => {
console.log('Button clicked!');
};
return (
<div>
<CustomButton onClick={handleClick}>
Click me
</CustomButton>
</div>
);
}
// ❌ Incorrect
function UserProfile(props) {
props.name = 'New name'; // ❌ Error!
return <div>{props.name}</div>;
}
// ✅ Correct
function UserProfile({ name }) {
const [localName, setLocalName] = useState(name);
return (
<div>
<span>{localName}</span>
<button onClick={() => setLocalName('New name')}>
Change
</button>
</div>
);
}
// ❌ Incorrect
function TodoList({ todos, onTodosChange }) {
const addTodo = (text) => {
todos.push({ id: Date.now(), text, completed: false }); // ❌ Mutation!
onTodosChange(todos);
};
return (
<div>
{todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
<button onClick={() => addTodo('New task')}>
Add
</button>
</div>
);
}
// ✅ Correct
function TodoList({ todos, onTodosChange }) {
const addTodo = (text) => {
const newTodos = [...todos, { id: Date.now(), text, completed: false }]; // ✅ New array
onTodosChange(newTodos);
};
return (
<div>
{todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
<button onClick={() => addTodo('New task')}>
Add
</button>
</div>
);
}
Props are data passed from a parent component to a child component in React, which are immutable by design:
✅ Why props are immutable:
❌ What you can’t do:
Key points:
Understanding props immutability is a fundamental React development skill that helps write predictable, maintainable, and efficient code.
Want more articles for interview preparation? Subscribe to EasyAdvice, bookmark the site, and improve every day 💪