frontend / react / state_vs_props.md

React State vs Props

4 interview angles 9 min read source

React State vs Props

Understanding the difference between state and props is crucial for React development. They are both ways to handle data in React components, but they serve different purposes and have different characteristics.

Table of Contents


What are Props?

Props (short for “properties”) are a way to pass data from parent components to child components. They are read-only and cannot be modified by the component that receives them.

Characteristics of Props:

  • Read-only: Cannot be modified by the receiving component
  • Immutable: Should not be changed within the component
  • Passed down: Flows from parent to child
  • Any data type: Can be strings, numbers, objects, functions, etc.
  • Default values: Can have default values

Props in Functional Components:

// Basic props
function Welcome(props) {
  return <h1>Hello, {props.name}!</h1>;
}

// Destructuring props
function Welcome({ name, age, isLoggedIn }) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>Age: {age}</p>
      <p>Status: {isLoggedIn ? 'Logged in' : 'Not logged in'}</p>
    </div>
  );
}

// Default props
function Welcome({ name = 'Guest', age = 0 }) {
  return <h1>Hello, {name}! You are {age} years old.</h1>;
}

Props in Class Components:

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}!</h1>;
  }
}

// With destructuring in render
class Welcome extends React.Component {
  render() {
    const { name, age } = this.props;
    return (
      <div>
        <h1>Hello, {name}!</h1>
        <p>Age: {age}</p>
      </div>
    );
  }
}

What is State?

State is a component’s internal data that can change over time. When state changes, the component re-renders to reflect the new data.

Characteristics of State:

  • Mutable: Can be changed within the component
  • Local: Belongs to the component that defines it
  • Triggers re-renders: Component re-renders when state changes
  • Asynchronous: State updates may be batched
  • Private: Not accessible from outside the component

State in Functional Components (Hooks):

import { useState } from 'react';

function Counter() {
  // Basic state
  const [count, setCount] = useState(0);

  // Multiple state variables
  const [name, setName] = useState('');
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  // Object state
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0
  });

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>

      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Enter name"
      />

      <button onClick={() => setIsLoggedIn(!isLoggedIn)}>
        {isLoggedIn ? 'Logout' : 'Login'}
      </button>
    </div>
  );
}

State in Class Components:

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
      name: '',
      isLoggedIn: false
    };
  }

  // Update state
  incrementCount = () => {
    this.setState({ count: this.state.count + 1 });
  };

  // Update state with function (when new state depends on old state)
  incrementCountSafely = () => {
    this.setState(prevState => ({
      count: prevState.count + 1
    }));
  };

  // Update multiple state properties
  updateUser = (name) => {
    this.setState({
      name: name,
      isLoggedIn: true
    });
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.incrementCount}>
          Increment
        </button>

        <input
          value={this.state.name}
          onChange={(e) => this.setState({ name: e.target.value })}
          placeholder="Enter name"
        />
      </div>
    );
  }
}

Key Differences

Aspect Props State
Mutability Read-only Mutable
Source Parent component Component itself
Data Flow Top-down (parent to child) Internal to component
Updates Parent component controls Component controls
Re-renders When parent re-renders When state changes
Access props.propertyName state.propertyName
Initialization Passed from parent Defined in component
Validation PropTypes No built-in validation

Props Example:

// Parent component
function App() {
  const userData = {
    name: 'John Doe',
    email: 'john@example.com',
    age: 30
  };

  return <UserProfile user={userData} />;
}

// Child component (cannot modify props)
function UserProfile({ user }) {
  // This won't work - props are read-only
  // user.name = 'Jane Doe';

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <p>Age: {user.age}</p>
    </div>
  );
}

State Example:

function UserForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    age: ''
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prevData => ({
      ...prevData,
      [name]: value
    }));
  };

  return (
    <form>
      <input
        name="name"
        value={formData.name}
        onChange={handleChange}
        placeholder="Name"
      />
      <input
        name="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
      />
      <input
        name="age"
        value={formData.age}
        onChange={handleChange}
        placeholder="Age"
      />
    </form>
  );
}

Usage Examples

Props for Configuration:

// Button component with props for configuration
function Button({
  children,
  variant = 'primary',
  size = 'medium',
  onClick,
  disabled = false
}) {
  const buttonClass = `btn btn-${variant} btn-${size}`;

  return (
    <button
      className={buttonClass}
      onClick={onClick}
      disabled={disabled}
    >
      {children}
    </button>
  );
}

// Usage
<Button variant="secondary" size="large" onClick={handleClick}>
  Click Me
</Button>

State for User Interaction:

function TodoList() {
  const [todos, setTodos] = useState([]);
  const [inputValue, setInputValue] = useState('');

  const addTodo = () => {
    if (inputValue.trim()) {
      setTodos(prevTodos => [
        ...prevTodos,
        { id: Date.now(), text: inputValue, completed: false }
      ]);
      setInputValue('');
    }
  };

  const toggleTodo = (id) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === id
          ? { ...todo, completed: !todo.completed }
          : todo
      )
    );
  };

  return (
    <div>
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="Add a todo"
      />
      <button onClick={addTodo}>Add</button>

      <ul>
        {todos.map(todo => (
          <li
            key={todo.id}
            onClick={() => toggleTodo(todo.id)}
            style={{
              textDecoration: todo.completed ? 'line-through' : 'none'
            }}
          >
            {todo.text}
          </li>
        ))}
      </ul>
    </div>
  );
}

Combining Props and State:

// Parent component with state
function App() {
  const [users, setUsers] = useState([
    { id: 1, name: 'John', email: 'john@example.com' },
    { id: 2, name: 'Jane', email: 'jane@example.com' }
  ]);

  const deleteUser = (id) => {
    setUsers(prevUsers => prevUsers.filter(user => user.id !== id));
  };

  return (
    <div>
      <h1>User List</h1>
      {users.map(user => (
        <UserCard
          key={user.id}
          user={user}
          onDelete={deleteUser}
        />
      ))}
    </div>
  );
}

// Child component with props
function UserCard({ user, onDelete }) {
  const [isExpanded, setIsExpanded] = useState(false);

  return (
    <div className="user-card">
      <h3>{user.name}</h3>
      {isExpanded && <p>{user.email}</p>}
      <button onClick={() => setIsExpanded(!isExpanded)}>
        {isExpanded ? 'Hide' : 'Show'} Email
      </button>
      <button onClick={() => onDelete(user.id)}>
        Delete
      </button>
    </div>
  );
}

Data Flow

Unidirectional Data Flow:

// Top-down data flow with props
function App() {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState({ name: 'John', role: 'admin' });

  return (
    <div className={`app ${theme}`}>
      <Header
        user={user}
        theme={theme}
        onThemeChange={setTheme}
      />
      <MainContent user={user} />
      <Footer theme={theme} />
    </div>
  );
}

function Header({ user, theme, onThemeChange }) {
  return (
    <header>
      <h1>Welcome, {user.name}</h1>
      <button onClick={() => onThemeChange(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </header>
  );
}

function MainContent({ user }) {
  const [posts, setPosts] = useState([]);

  // Local state for this component
  useEffect(() => {
    // Fetch posts based on user
    fetchPosts(user.id).then(setPosts);
  }, [user.id]);

  return (
    <main>
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </main>
  );
}

Lifting State Up:

// When multiple components need the same state
function App() {
  const [selectedUser, setSelectedUser] = useState(null);

  return (
    <div>
      <UserList
        selectedUser={selectedUser}
        onUserSelect={setSelectedUser}
      />
      <UserDetails user={selectedUser} />
    </div>
  );
}

function UserList({ selectedUser, onUserSelect }) {
  const [users, setUsers] = useState([]);

  return (
    <div>
      {users.map(user => (
        <div
          key={user.id}
          className={selectedUser?.id === user.id ? 'selected' : ''}
          onClick={() => onUserSelect(user)}
        >
          {user.name}
        </div>
      ))}
    </div>
  );
}

function UserDetails({ user }) {
  if (!user) return <p>Select a user to see details</p>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

Best Practices

1. Use Props for Configuration:

// Good: Props for configuration
function Modal({
  isOpen,
  title,
  children,
  onClose,
  size = 'medium'
}) {
  if (!isOpen) return null;

  return (
    <div className={`modal modal--${size}`}>
      <div className="modal-header">
        <h2>{title}</h2>
        <button onClick={onClose}>×</button>
      </div>
      <div className="modal-body">
        {children}
      </div>
    </div>
  );
}

// Usage
<Modal
  isOpen={showModal}
  title="Confirm Delete"
  onClose={() => setShowModal(false)}
  size="small"
>
  Are you sure you want to delete this item?
</Modal>

2. Use State for User Interaction:

// Good: State for user interaction
function SearchBar({ onSearch }) {
  const [query, setQuery] = useState('');
  const [isLoading, setIsLoading] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsLoading(true);
    await onSearch(query);
    setIsLoading(false);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
        disabled={isLoading}
      />
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Searching...' : 'Search'}
      </button>
    </form>
  );
}

3. Don’t Modify Props:

// Bad: Modifying props
function UserProfile({ user }) {
  // Don't do this!
  user.name = 'Modified Name';

  return <h1>{user.name}</h1>;
}

// Good: Use state if you need to modify
function UserProfile({ user }) {
  const [localUser, setLocalUser] = useState(user);

  const updateName = (newName) => {
    setLocalUser(prev => ({ ...prev, name: newName }));
  };

  return (
    <div>
      <h1>{localUser.name}</h1>
      <input
        value={localUser.name}
        onChange={(e) => updateName(e.target.value)}
      />
    </div>
  );
}

4. Use Functional Updates for State:

// Good: Functional updates when new state depends on old state
function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };

  const incrementBy = (amount) => {
    setCount(prevCount => prevCount + amount);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={() => incrementBy(5)}>Increment by 5</button>
    </div>
  );
}

Common Interview Questions

Q: What is the difference between props and state?

  • Props are read-only data passed from parent to child, while state is mutable data managed within a component.

Q: Can you modify props in a React component?

  • No, props are read-only and should not be modified. They are immutable.

Q: How do you update state in functional components?

  • Using the setter function returned by useState hook (e.g., setCount(newValue)).

Q: How do you update state in class components?

  • Using this.setState() method, which can take an object or a function.

Q: What happens when state changes?

  • The component re-renders to reflect the new state values.

Q: Can you pass state as props?

  • Yes, you can pass state values as props to child components.

Q: What is the data flow in React?

  • Unidirectional: data flows from parent to child via props, and state changes trigger re-renders.

Q: When should you use props vs state?

  • Use props for configuration and data passed from parent, use state for component-specific data that changes over time.

Q: How do you handle state that needs to be shared between components?

  • Lift state up to a common parent component or use state management libraries like Redux or Context API.

Summary

  • Props are read-only data passed from parent to child components
  • State is mutable data managed within a component that triggers re-renders when changed
  • Props flow down the component tree, while state is local to the component
  • Use props for configuration and data passing, use state for user interaction and component-specific data
  • Never modify props directly; use state if you need to modify data
  • Understanding the distinction between props and state is essential for effective React development

Interview angle

  • “State or props?” - props are inputs from the parent, read-only inside the component; state is owned locally and mutated only through its setter. If a value can be computed from props or other state, it should not be state.
  • “What is the most common state mistake?” - copying props into state, which then goes stale when the prop changes. Either derive it during render, or if you truly need to reset on change, key the component.
  • “Where should state live?” - the lowest common ancestor of everything that reads it. Hoisting state higher than necessary is what makes whole subtrees re-render on every keystroke.
  • “Why must you not mutate state?” - React compares by reference to decide whether to re-render. arr.push(x); setArr(arr) passes the same reference, so nothing updates. Use a new array or object - toSorted, spread, or an immutable helper.