SB

React Props vs State: When to Use What?

React Props vs State: When to Use What?

Introduction

Props and state are the foundation of data flow in React applications. At first glance, they might seem interchangeable, but each serves a distinct purpose in the component architecture. The key to building maintainable React applications lies in understanding not just what they do, but when and why to use each one.

After working with React for several years, I’ve seen countless codebases where props and state were used incorrectly, leading to unnecessary re-renders, confusing data flows, and components that are hard to debug. This guide will help you avoid those pitfalls and make informed decisions about data management in your React components.

What Are Props in React?

Props (properties) are React’s way of passing data from parent to child components. Think of them as function arguments for your components. Once a prop is passed down, the receiving component can read it but cannot modify it directly.

Key Characteristics

  • Immutable: Props are read-only from the child component’s perspective
  • Unidirectional: Data flows down from parent to child
  • Functional: Enable component composition and reusability
  • Type-safe: Can be validated with PropTypes or TypeScript

Practical Props Example

// Parent Component
function UserDashboard() {
  const user = { 
    name: 'Sarah Chen', 
    role: 'Frontend Developer',
    avatar: '/avatars/sarah.jpg' 
  };

  const handleUserUpdate = (updatedData) => {
    // Handle user updates in parent
    console.log('User updated:', updatedData);
  };

  return (
    <div>
      <UserProfile 
        user={user} 
        onUpdate={handleUserUpdate}
        isEditable={true}
      />
      <UserStats userId={user.id} />
    </div>
  );
}

// Child Component
function UserProfile({ user, onUpdate, isEditable }) {
  return (
    <div className="user-profile">
      <img src={user.avatar} alt={`${user.name}'s avatar`} />
      <h2>{user.name}</h2>
      <p>{user.role}</p>
      {isEditable && (
        <button onClick={() => onUpdate({ name: 'New Name' })}>
          Edit Profile
        </button>
      )}
    </div>
  );
}

When Props Excel

Props are your go-to solution for:

  • Configuring component behavior from the parent
  • Passing callback functions for event handling
  • Creating reusable components with different configurations
  • Implementing the container/presentational component pattern

What Is State in React?

State represents data that belongs to a component and can change over time. It’s the component’s private memory that triggers re-renders when updated. With hooks, we primarily use useState for local component state.

Key Characteristics

  • Mutable: Can be updated using state setters
  • Local: Belongs to the component that declares it
  • Reactive: Updates trigger component re-renders
  • Asynchronous: State updates may be batched or deferred

Real-World State Example

import { useState, useEffect } from 'react';

function SearchableProductList() {
  const [products, setProducts] = useState([]);
  const [searchTerm, setSearchTerm] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchProducts = async () => {
      setLoading(true);
      setError(null);
      
      try {
        const response = await fetch('/api/products');
        const data = await response.json();
        setProducts(data);
      } catch (err) {
        setError('Failed to load products');
      } finally {
        setLoading(false);
      }
    };

    fetchProducts();
  }, []);

  const filteredProducts = products.filter(product =>
    product.name.toLowerCase().includes(searchTerm.toLowerCase())
  );

  const handleSearchChange = (e) => {
    setSearchTerm(e.target.value);
  };

  if (loading) return <div>Loading products...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div>
      <input
        type="text"
        placeholder="Search products..."
        value={searchTerm}
        onChange={handleSearchChange}
      />
      <ProductList products={filteredProducts} />
    </div>
  );
}

State Management Patterns

State works best for:

  • Form inputs and validation
  • Toggle states (modals, dropdowns, accordions)
  • Loading and error states
  • Client-side data filtering and sorting
  • Component-specific UI states

Props vs State: Complete Comparison

FeaturePropsStateUsage NotesCommon Pitfalls
OwnershipPassed from parentBelongs to componentProps come from above, state lives withinDon’t mix ownership responsibilities
MutabilityRead-onlyCan be updatedNever mutate props directlyAvoid direct state mutations
Data FlowTop-down onlyInternal to componentProps enable compositionState should be lifted when shared
Re-rendersTrigger when parent updatesTrigger when setState calledBoth cause re-renders of childrenMinimize unnecessary state updates
TestingEasy to mockRequires interaction simulationProps are simpler to testState testing needs more setup
PerformanceEfficient when staticCan cause performance issuesUse React.memo for props optimizationConsider useCallback for state setters

Advanced Props Patterns

Render Props Pattern

function DataFetcher({ url, children }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    fetch(url)
      .then(res => res.json())
      .then(setData)
      .finally(() => setLoading(false));
  }, [url]);

  return children({ data, loading });
}

// Usage
<DataFetcher url="/api/users">
  {({ data, loading }) => (
    loading ? <Spinner /> : <UserList users={data} />
  )}
</DataFetcher>

Compound Components

function Modal({ children, isOpen }) {
  if (!isOpen) return null;

  return (
    <div className="modal-overlay">
      <div className="modal-content">
        {children}
      </div>
    </div>
  );
}

Modal.Header = ({ children }) => (
  <div className="modal-header">{children}</div>
);

Modal.Body = ({ children }) => (
  <div className="modal-body">{children}</div>
);

Modal.Footer = ({ children }) => (
  <div className="modal-footer">{children}</div>
);

// Usage
<Modal isOpen={showModal}>
  <Modal.Header>Confirm Delete</Modal.Header>
  <Modal.Body>Are you sure you want to delete this item?</Modal.Body>
  <Modal.Footer>
    <button onClick={handleCancel}>Cancel</button>
    <button onClick={handleConfirm}>Delete</button>
  </Modal.Footer>
</Modal>

State Management Best Practices

State Collocation

Keep state as close to where it’s used as possible:

// ❌ Bad - Lifting state too high
function App() {
  const [modalOpen, setModalOpen] = useState(false);
  const [formData, setFormData] = useState({});
  
  return (
    <div>
      <Header />
      <MainContent />
      <SomeDeepComponent 
        modalOpen={modalOpen} 
        setModalOpen={setModalOpen}
        formData={formData}
        setFormData={setFormData}
      />
    </div>
  );
}

// ✅ Good - State where it's needed
function SomeDeepComponent() {
  const [modalOpen, setModalOpen] = useState(false);
  const [formData, setFormData] = useState({});
  
  return (
    <div>
      <button onClick={() => setModalOpen(true)}>Open Modal</button>
      {modalOpen && <FormModal data={formData} onSubmit={setFormData} />}
    </div>
  );
}

Derived State Anti-Pattern

// ❌ Bad - Duplicating props in state
function UserProfile({ user }) {
  const [userName, setUserName] = useState(user.name);
  
  useEffect(() => {
    setUserName(user.name);
  }, [user.name]);
  
  return <h1>{userName}</h1>;
}

// ✅ Good - Use props directly or derive state
function UserProfile({ user }) {
  const displayName = user.name.toUpperCase();
  return <h1>{displayName}</h1>;
}

Common Decision Framework

Use Props When:

  1. Data comes from parent component
  2. Component needs to be reusable with different configurations
  3. Implementing callback patterns for event handling
  4. Creating presentational components

Use State When:

  1. Data changes based on user interaction
  2. Managing component-specific UI states
  3. Handling asynchronous operations (loading, error states)
  4. Implementing controlled form inputs

Performance Considerations

💡 Tip: Props changes cause re-renders of all child components. Use React.memo() to prevent unnecessary re-renders when props haven’t actually changed.

const ExpensiveComponent = React.memo(({ data, onUpdate }) => {
  console.log('ExpensiveComponent rendered');
  return (
    <div>
      {data.map(item => <Item key={item.id} item={item} />)}
    </div>
  );
});

// Only re-renders when data or onUpdate reference changes

⚠️ Warning: Avoid creating objects or functions inline in JSX when passing as props, as this creates new references on every render.

// ❌ Bad - Creates new object every render
<UserProfile user={{ name: 'John', age: 30 }} />

// ✅ Good - Stable reference
const user = { name: 'John', age: 30 };
<UserProfile user={user} />

Debugging Props and State

React Developer Tools

The React DevTools browser extension is invaluable for debugging:

  • Inspect props and state values in real-time
  • Track which props changed between renders
  • Monitor state updates and their timing

Common Debugging Patterns

function DebuggableComponent({ complexProp }) {
  const [count, setCount] = useState(0);
  
  // Log props changes
  useEffect(() => {
    console.log('complexProp changed:', complexProp);
  }, [complexProp]);
  
  // Log state changes
  useEffect(() => {
    console.log('count changed:', count);
  }, [count]);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  );
}

Migration Strategies

When refactoring between props and state:

Props to State (Adding Interactivity)

// Before: Static component with props
function TodoItem({ todo }) {
  return (
    <li>
      <span>{todo.text}</span>
      {todo.completed && <span></span>}
    </li>
  );
}

// After: Interactive component with state
function TodoItem({ todo, onToggle }) {
  const [isEditing, setIsEditing] = useState(false);
  const [editText, setEditText] = useState(todo.text);
  
  const handleSave = () => {
    onToggle(todo.id, { ...todo, text: editText });
    setIsEditing(false);
  };
  
  return (
    <li>
      {isEditing ? (
        <input 
          value={editText} 
          onChange={(e) => setEditText(e.target.value)}
          onBlur={handleSave}
        />
      ) : (
        <span onClick={() => setIsEditing(true)}>{todo.text}</span>
      )}
    </li>
  );
}

State to Props (Lifting State Up)

// Before: Each item manages its own selection state
function TodoItem({ todo }) {
  const [isSelected, setIsSelected] = useState(false);
  // Problem: Parent can't access selection state
}

// After: Selection managed by parent
function TodoList() {
  const [selectedItems, setSelectedItems] = useState(new Set());
  
  const handleToggleSelection = (itemId) => {
    setSelectedItems(prev => {
      const next = new Set(prev);
      if (next.has(itemId)) {
        next.delete(itemId);
      } else {
        next.add(itemId);
      }
      return next;
    });
  };
  
  return (
    <div>
      {todos.map(todo => (
        <TodoItem 
          key={todo.id}
          todo={todo}
          isSelected={selectedItems.has(todo.id)}
          onToggleSelection={handleToggleSelection}
        />
      ))}
    </div>
  );
}

Conclusion

Mastering props and state is about understanding data ownership and flow in your React applications. Props excel at component configuration and composition, while state handles dynamic, interactive data within components.

The decision between props and state often comes down to asking: “Who owns this data and who needs to change it?” When multiple components need access to the same data, lift state to their common parent. When data is only relevant to a single component and its immediate children, keep it local with state.

Remember that good React architecture often starts with state in components and gradually lifts it up as requirements evolve. Don’t over-engineer early, but be prepared to refactor as your application grows.

About Author

Maulik Paghdal

I'm Maulik Paghdal, the founder of Script Binary and a passionate full-stack web developer. I have a strong foundation in both frontend and backend development, specializing in building dynamic, responsive web applications using Laravel, Vue.js, and React.js. With expertise in Tailwind CSS and Bootstrap, I focus on creating clean, efficient, and scalable solutions that enhance user experiences and optimize performance.

Topics