What are props and why are props immutable?

👨‍💻 Frontend Developer 🟠 May come up 🎚️ Medium
#React

Brief Answer

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:

  • Unidirectional data flow (top-down)
  • Prevention of side effects
  • Simplified debugging and testing
  • Performance improvements (through comparison mechanism)

What you can’t do with props:

  • Directly modify them (props.name = 'new name')
  • Mutate objects and arrays in props
  • Delete properties from props

Key rule: A component can read props but cannot change them - the parent component is responsible for that.


Full Answer

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.

What are props

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>
  );
}

Why props are immutable

1. Unidirectional data flow

React uses unidirectional data flow, where data flows from top to bottom:

// ❌ Incorrect - attempting to&nbsp;modify props
function ChildComponent(props) {
  // props.name = 'New name'; // ❌ Error! Props cannot be&nbsp;changed
  
  return <div>Hello, {props.name}!</div>;
}
 
// ✅ Correct - component simply uses props
function ChildComponent({ name }) {
  return <div>Hello, {name}!</div>;
}
 
// If you need to&nbsp;change data, use state
function ParentComponent() {
  const [name, setName] = useState('Alexander');
  
  return (
    <div>
      <ChildComponent name={name} />
      <button onClick={() => setName('Anna')}>
        Change name
      </button>
    </div>
  );
}

2. Prevention of side effects

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&nbsp;copy for changes
function GoodComponent({ user }) {
  // Create a&nbsp;copy for safe changes
  const modifiedUser = { ...user, name: 'New name' };
  
  return <div>{modifiedUser.name}</div>;
}

3. Simplified debugging

Immutability simplifies debugging and understanding data flow:

// Easy to&nbsp;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>
  );
}

4. Performance improvements

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&nbsp;reference comparison

How to properly work with props

1. Reading props

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>
  );
}

2. Passing props

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>
  );
}

3. Working with mutable data

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>
  );
}

Practical Examples

1. Working with objects and arrays in props

// ❌ 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&nbsp;new object
function TodoItem({ todo, onUpdate }) {
  const toggleComplete = () => {
    // Create a&nbsp;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>
  );
}

2. Children props

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>
  );
}

3. Functions as props

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>
  );
}

Common Mistakes

1. Attempting to modify props directly

// ❌ 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>
  );
}

2. Mutating objects and arrays

// ❌ 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>
  );
}

Summary

Props are data passed from a parent component to a child component in React, which are immutable by design:

Why props are immutable:

  • Ensure unidirectional data flow
  • Prevent side effects
  • Simplify debugging and testing
  • Improve performance

What you can’t do:

  • Directly modify props
  • Mutate objects and arrays from props
  • Call functions from props in component body

Key points:

  • Components can read props but cannot change them
  • Use state (useState) to change data
  • Create new copies when working with objects and arrays from props
  • Use destructuring for convenient work with props

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 💪