Skip to main content

Props vs State vs Context vs Ref: When to Use Each in React

Props vs State vs Context vs Ref: When to Use Each in React
react#react#props#state#context+4 more

Props vs State vs Context vs Ref: When to Use Each in React

Confused about when to use Props, State, Context, or Ref? Misusing them doesn't just slow performance—it creates tangled code that's hard to debug. Let's clear things up once and for all.

Mohammad Alhabil

Author

December 18, 2024
5 min read
~1000 words

Props vs State vs Context vs Ref: When to Use Each in React

Props? State? Context? Ref? When should you use each one?

Misusing them doesn't just slow down performance... it creates tangled code that becomes a nightmare to debug later!

Let's sort things out once and for all and understand the differences between them, and when to use each.

1️⃣ Props – The Delivery Mechanism

Think of props like a message from parent to child... you're just delivering, not deciding.

  • Data flows from Parent → Child
  • Cannot be changed inside the child, only displayed or interacted with
  • Use when: The child component just needs to display the value, not modify it

Example

You have a products page, and you're sending each <ProductCard /> information like title and price as props:

// Parent Component
function ProductsPage() {
  const products = [
    { id: 1, title: 'Laptop', price: 999 },
    { id: 2, title: 'Phone', price: 699 },
  ];

  return (
    <div className="grid grid-cols-3 gap-4">
      {products.map(product => (
        <ProductCard 
          key={product.id}
          title={product.title}   // ← Props going down
          price={product.price}   // ← Props going down
        />
      ))}
    </div>
  );
}

// Child Component
function ProductCard({ title, price }: { title: string; price: number }) {
  // Can only READ these values, not change them
  return (
    <div className="p-4 border rounded">
      <h3>{title}</h3>
      <p>${price}</p>
    </div>
  );
}

2️⃣ State – When the Component is in Charge

Think of state like your personal notebook that you write and edit as you please.

  • Stores data that can change internally
  • Every time it changes, the component re-renders
  • Use when: The component manages its own internal state and doesn't share it with others

Example

A modal with an internal form, controlling input values using useState:

function ContactModal() {
  // State is OWNED by this component
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async () => {
    setIsSubmitting(true);
    await sendEmail({ name, email });
    setIsSubmitting(false);
  };

  return (
    <div className="modal">
      <input 
        value={name}
        onChange={(e) => setName(e.target.value)}  // Component controls its own state
        placeholder="Your name"
      />
      <input 
        value={email}
        onChange={(e) => setEmail(e.target.value)}  // Component controls its own state
        placeholder="Your email"
      />
      <button onClick={handleSubmit} disabled={isSubmitting}>
        {isSubmitting ? 'Sending...' : 'Send'}
      </button>
    </div>
  );
}

3️⃣ Context – When Everyone Needs to See the Same Thing

Think of context like electricity... one source that reaches all rooms.

  • Pass shared data to multiple components without props drilling
  • Use when: You need global state within React, without external libraries

Example

Theme (dark/light) or user data after login:

// 1. Create the Context
const ThemeContext = createContext<{
  theme: 'light' | 'dark';
  toggleTheme: () => void;
} | null>(null);

// 2. Create the Provider
function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');
  
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. Use anywhere in the app!
function Header() {
  const { theme, toggleTheme } = useContext(ThemeContext)!;
  
  return (
    <header className={theme === 'dark' ? 'bg-gray-900' : 'bg-white'}>
      <button onClick={toggleTheme}>
        Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
      </button>
    </header>
  );
}

function Footer() {
  const { theme } = useContext(ThemeContext)!;
  // Footer also has access to theme without props drilling!
  return <footer className={theme === 'dark' ? 'bg-gray-900' : 'bg-white'}>...</footer>;
}

⚠️ Important Warning

Context triggers a re-render for every component that reads from it. If you have rapidly changing data (like a counter or timer), don't use Context as it can hurt performance.

// ❌ BAD - Counter updates every second, re-renders ALL consumers
const TimerContext = createContext(0);

function TimerProvider({ children }) {
  const [seconds, setSeconds] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => setSeconds(s => s + 1), 1000);
    return () => clearInterval(interval);
  }, []);

  return (
    <TimerContext.Provider value={seconds}>
      {children}  {/* Every consumer re-renders every second! */}
    </TimerContext.Provider>
  );
}

// ✅ BETTER - Use state locally or a state management library

4️⃣ Ref – When You Need to Deal with Things "As They Are"

Think of ref like a surveillance camera... watching the scene without changing it.

  • Stores values without causing re-renders
  • Perfect for DOM interaction or keeping stable references
  • Use when: You need to keep a value between renders without updating the UI, or interact with DOM directly (like controlling input focus)

Example 1: Auto-focus on Modal Open

function LoginModal({ isOpen }: { isOpen: boolean }) {
  const emailInputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    if (isOpen && emailInputRef.current) {
      emailInputRef.current.focus();  // Direct DOM manipulation
    }
  }, [isOpen]);

  return (
    <div className="modal">
      <input 
        ref={emailInputRef}  // Attach ref to DOM element
        type="email"
        placeholder="Enter your email"
      />
      <input type="password" placeholder="Password" />
      <button>Login</button>
    </div>
  );
}

Example 2: AbortController for Fetch Requests

function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const abortControllerRef = useRef<AbortController | null>(null);

  useEffect(() => {
    // Cancel previous request if still pending
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }

    // Create new AbortController
    abortControllerRef.current = new AbortController();

    const fetchResults = async () => {
      try {
        const response = await fetch(`/api/search?q=${query}`, {
          signal: abortControllerRef.current!.signal,
        });
        const data = await response.json();
        setResults(data);
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error('Search failed:', error);
        }
      }
    };

    if (query) fetchResults();
  }, [query]);

  return (
    <div>
      <input 
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <ul>
        {results.map(item => <li key={item.id}>{item.name}</li>)}
      </ul>
    </div>
  );
}

💡 Want to learn how AbortController doesn't just cancel fetch requests... but can improve performance and solve many problems? Stay tuned for the next post!

The Golden Question

For every variable in your project, ask yourself:

"Who needs to control it? And who needs to see it?"

From that question, you decide where to put it:

QuestionAnswer
Just needs to display it?Props
Needs to change internally?State
Shared data needed across many components?Context
Need to interact with DOM or keep value without render?Ref

Quick Decision Flowchart

Is the data coming from a parent?
├── YES → Use Props
└── NO → Does it need to trigger re-renders when changed?
          ├── YES → Do multiple components need it?
          │         ├── YES → Use Context (or state management)
          │         └── NO → Use State
          └── NO → Use Ref

⚠️ Anti-Pattern: Passing setState as Props

// ❌ BAD - Passing setState directly
function Parent() {
  const [user, setUser] = useState(null);
  
  return <LoginForm setUser={setUser} />;  // Fragile!
}

// ✅ BETTER - Wrap in a custom hook or Context
function useAuth() {
  const [user, setUser] = useState(null);
  
  const login = async (credentials) => {
    const user = await authenticate(credentials);
    setUser(user);
  };
  
  const logout = () => setUser(null);
  
  return { user, login, logout };
}

// Or use Context
const AuthContext = createContext(null);

function AuthProvider({ children }) {
  const auth = useAuth();
  return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>;
}

Why is passing setState directly a bad practice?

  • ❌ Fragile code that's hard to track
  • ❌ Difficult to modify long-term
  • ❌ Breaks encapsulation
  • ❌ Makes testing harder

Summary Comparison Table

FeaturePropsStateContextRef
DirectionParent → ChildInternalGlobalInternal
Causes Re-renderWhen parent changesYesYes (all consumers)No
MutableNoYes (via setter)Yes (via setter)Yes (directly)
Best ForPassing data downLocal UI stateShared global stateDOM/values without render

Pro Tips

  1. Start with props – Only elevate to state/context when needed
  2. Keep state close – Put state as close to where it's used as possible
  3. Split contexts – Don't put everything in one giant context
  4. Memoize context values – Prevent unnecessary re-renders
  5. Use refs sparingly – They're escape hatches, not the default

Interfaces you love, code you understand. 💡

Topics covered

#react#props#state#context#useref#hooks#performance#best-practices

Found this article helpful?

Share it with your network and help others learn too!

Mohammad Alhabil

Written by Mohammad Alhabil

Frontend Developer & Software Engineer passionate about building beautiful and functional web experiences. I write about React, Next.js, and modern web development.