Ojasa Mirai

Ojasa Mirai

ReactJS

Loading...

Learning Level

🟒 BeginnerπŸ”΅ Advanced
πŸ’Ύ Introduction to Stateβš›οΈ Using useState HookπŸ”„ Updating State Correctly🎯 Initial State Values🚫 Common State MistakesπŸ“Š Multiple State VariablesπŸ”— State & RenderingπŸ“ Forms with StateπŸ—οΈ State Structure Best Practices
Reactjs/State/State Vs Props

🌊 State vs Props vs Context β€” When to Use Each and Architectural Patterns

Understanding when to use state, props, or context fundamentally impacts application architecture. Each serves different purposes and has trade-offs. Master these distinctions to build scalable applications.


🎯 State vs Props: Ownership and Responsibility

State is owned by a component and can change. Props are passed from parent and are read-only. The decision between them determines data flow and component reusability.

State lives in a component. Props flow down from parent. Understand which tool solves which problem.

import { useState } from 'react';

// ❌ WRONG: Putting everything in props
// Parent must manage all state
function BadParent() {
  const [counter, setCounter] = useState(0);
  const [name, setName] = useState('');
  const [isOpen, setIsOpen] = useState(false);
  const [filters, setFilters] = useState({});

  return (
    <BadChild
      counter={counter}
      setCounter={setCounter}
      name={name}
      setName={setName}
      isOpen={isOpen}
      setIsOpen={setIsOpen}
      filters={filters}
      setFilters={setFilters}
    />
  );
}

// Props are too verbose, makes reuse harder
function BadChild(props: any) {
  return (
    <div>
      <p>Counter: {props.counter}</p>
      <button onClick={() => props.setCounter(props.counter + 1)}>+</button>
      <input
        value={props.name}
        onChange={(e) => props.setName(e.target.value)}
      />
    </div>
  );
}

// βœ… CORRECT: Component manages its own state
// Parent passes minimal required props
function GoodParent() {
  return (
    <div>
      <GoodCounter />
      <GoodForm />
      <GoodModal />
    </div>
  );
}

function GoodCounter() {
  const [count, setCount] = useState(0);

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

function GoodForm() {
  const [name, setName] = useState('');

  return (
    <div>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Name"
      />
      <p>Hello {name}</p>
    </div>
  );
}

function GoodModal() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>Toggle Modal</button>
      {isOpen && <p>Modal content</p>}
    </div>
  );
}

<details>

<summary>πŸ“š More Examples</summary>

// Example: Props for configuration, state for interaction

interface ButtonProps {
  variant: 'primary' | 'secondary'; // Prop: doesn't change
  size: 'small' | 'medium' | 'large'; // Prop: configuration
  onClick?: () => void; // Prop: callback
}

function Button({ variant, size, onClick }: ButtonProps) {
  const [isPressed, setIsPressed] = useState(false); // State: internal

  const handleClick = () => {
    setIsPressed(true);
    onClick?.();
    setTimeout(() => setIsPressed(false), 100);
  };

  return (
    <button
      onClick={handleClick}
      className={`btn-${variant} btn-${size} ${isPressed ? 'pressed' : ''}`}
    >
      Click me
    </button>
  );
}

// Example: Lifting state vs prop drilling

// ❌ BAD: Prop drilling (passing through many levels)
function BadShoppingCart() {
  const [items, setItems] = useState<string[]>([]);

  return (
    <Checkout
      items={items}
      onAddItem={(item) => setItems([...items, item])}
      onRemoveItem={(item) => setItems(items.filter((i) => i !== item))}
    />
  );
}

function Checkout({ items, onAddItem, onRemoveItem }: any) {
  return (
    <div>
      <CartList items={items} onRemoveItem={onRemoveItem} />
      <ProductList onAddItem={onAddItem} />
    </div>
  );
}

function CartList({ items, onRemoveItem }: any) {
  return (
    <div>
      {items.map((item) => (
        <CartItem key={item} item={item} onRemove={onRemoveItem} />
      ))}
    </div>
  );
}

// βœ… GOOD: Use Context to avoid prop drilling
import { createContext, useContext } from 'react';

interface CartContextType {
  items: string[];
  addItem: (item: string) => void;
  removeItem: (item: string) => void;
}

const CartContext = createContext<CartContextType | undefined>(undefined);

function GoodShoppingCart() {
  const [items, setItems] = useState<string[]>([]);

  return (
    <CartContext.Provider
      value={{
        items,
        addItem: (item) => setItems([...items, item]),
        removeItem: (item) => setItems(items.filter((i) => i !== item)),
      }}
    >
      <Checkout />
    </CartContext.Provider>
  );
}

function Checkout() {
  return (
    <div>
      <CartList />
      <ProductList />
    </div>
  );
}

function CartList() {
  const cart = useContext(CartContext);
  return (
    <div>
      {cart?.items.map((item) => (
        <CartItem key={item} item={item} />
      ))}
    </div>
  );
}

function CartItem({ item }: { item: string }) {
  const cart = useContext(CartContext);
  return (
    <div>
      {item}
      <button onClick={() => cart?.removeItem(item)}>Remove</button>
    </div>
  );
}

</details>

πŸ’‘ Local State vs Shared State

Some state is local to a component (UI state like modals). Some state is shared across multiple components. Use props or context accordingly.

Local UI state should stay in the component. Shared business state should be lifted or put in context.

import { useState, createContext, useContext } from 'react';

// Local state: stays in component
function Modal() {
  const [isOpen, setIsOpen] = useState(false);
  const [selectedTab, setSelectedTab] = useState(0);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>
        {isOpen ? 'Close' : 'Open'}
      </button>
      {isOpen && (
        <div>
          <div>
            <button
              onClick={() => setSelectedTab(0)}
              className={selectedTab === 0 ? 'active' : ''}
            >
              Tab 1
            </button>
            <button
              onClick={() => setSelectedTab(1)}
              className={selectedTab === 1 ? 'active' : ''}
            >
              Tab 2
            </button>
          </div>
          {selectedTab === 0 && <p>Content 1</p>}
          {selectedTab === 1 && <p>Content 2</p>}
        </div>
      )}
    </div>
  );
}

// Shared state: lifted or in context
interface User {
  id: string;
  name: string;
  email: string;
}

const UserContext = createContext<{
  user: User | null;
  setUser: (user: User | null) => void;
} | null>(null);

function useUser() {
  const context = useContext(UserContext);
  if (!context) throw new Error('useUser must be inside UserProvider');
  return context;
}

function App() {
  const [user, setUser] = useState<User | null>(null);

  return (
    <UserContext.Provider value={{ user, setUser }}>
      <Header />
      <Content />
      <Footer />
    </UserContext.Provider>
  );
}

function Header() {
  const { user } = useUser();
  return <header>Welcome {user?.name}</header>;
}

function Content() {
  const { user } = useUser();
  return <main>{user ? 'Logged in' : 'Not logged in'}</main>;
}

function Footer() {
  const { user, setUser } = useUser();
  return (
    <footer>
      {user && (
        <button onClick={() => setUser(null)}>Logout</button>
      )}
    </footer>
  );
}

<details>

<summary>πŸ“š More Examples</summary>

// Example: Distinguishing local vs shared in a real app

interface NotificationContextType {
  notifications: Array<{ id: string; message: string }>;
  addNotification: (message: string) => void;
  removeNotification: (id: string) => void;
}

const NotificationContext = createContext<NotificationContextType | null>(null);

// Shared: notifications should be accessible from anywhere
function useNotifications() {
  const context = useContext(NotificationContext);
  if (!context) throw new Error('useNotifications must be inside provider');
  return context;
}

function NotificationManager() {
  const [notifications, setNotifications] = useState<Array<{ id: string; message: string }>>([]);

  const addNotification = (message: string) => {
    const id = Date.now().toString();
    setNotifications((prev) => [...prev, { id, message }]);
    setTimeout(() => removeNotification(id), 5000);
  };

  const removeNotification = (id: string) => {
    setNotifications((prev) => prev.filter((n) => n.id !== id));
  };

  return (
    <NotificationContext.Provider value={{ notifications, addNotification, removeNotification }}>
      {/* Children */}
    </NotificationContext.Provider>
  );
}

// Local state in component
function ToastContainer() {
  const { notifications } = useNotifications();
  const [animatingOut, setAnimatingOut] = useState<string[]>([]);

  return (
    <div>
      {notifications.map((notif) => (
        <Toast
          key={notif.id}
          message={notif.message}
          isAnimating={animatingOut.includes(notif.id)}
        />
      ))}
    </div>
  );
}

function Toast({ message, isAnimating }: any) {
  return <div className={isAnimating ? 'slide-out' : 'slide-in'}>{message}</div>;
}

</details>

πŸ”§ Context Patterns and Anti-patterns

Context is powerful but easy to misuse. Use it strategically for shared state, but avoid making it a catch-all for every state.

Context should hold truly global state: user, theme, notifications. Not every piece of state that's used in multiple components.

import { createContext, useContext, useState } from 'react';

// βœ… GOOD: Context for truly global state
interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | null>(null);

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

function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) throw new Error('useTheme must be inside ThemeProvider');
  return context;
}

// ❌ WRONG: Over-using context for every state
// This causes unnecessary re-renders
interface BadEverythingContextType {
  theme: string;
  user: any;
  filters: any;
  searchQuery: string;
  sortOrder: string;
  itemsPerPage: number;
  currentPage: number;
  // Too many unrelated states!
}

// βœ… GOOD: Split contexts by concern
interface SearchContextType {
  query: string;
  setQuery: (query: string) => void;
  sortOrder: 'asc' | 'desc';
  setSortOrder: (order: 'asc' | 'desc') => void;
}

const SearchContext = createContext<SearchContextType | null>(null);

interface PaginationContextType {
  itemsPerPage: number;
  currentPage: number;
  setPage: (page: number) => void;
}

const PaginationContext = createContext<PaginationContextType | null>(null);

// Example: Combining multiple contexts
function SearchResults() {
  return (
    <SearchProvider>
      <PaginationProvider>
        <Results />
      </PaginationProvider>
    </SearchProvider>
  );
}

πŸ“Š Data Flow Pattern Comparison

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Use Case     β”‚ State        β”‚ Props        β”‚ Context      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Local UI     β”‚ βœ… Best      β”‚ ❌           β”‚ ❌           β”‚
β”‚ Component    β”‚              β”‚              β”‚              β”‚
β”‚ Parentβ†’Child β”‚ ❌           β”‚ βœ… Best      β”‚ ❌           β”‚
β”‚ Single level β”‚              β”‚              β”‚              β”‚
β”‚ Global state β”‚ ❌           β”‚ ❌           β”‚ βœ… Best      β”‚
β”‚ Many levels  β”‚              β”‚              β”‚              β”‚
β”‚ Shared data  β”‚ ⚠️ Maybe     β”‚ ⚠️ Prop drillβ”‚ βœ… Best      β”‚
β”‚ Multiple     β”‚              β”‚              β”‚              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ”‘ Key Takeaways

  • βœ… Local UI state stays in components (modals, tabs, animations)
  • βœ… Pass props down one level for configuration
  • βœ… Use context for truly global state (user, theme)
  • βœ… Avoid prop drilling through many levels
  • βœ… Avoid over-using context for every piece of state
  • βœ… Split contexts by concern (Theme, User, Auth, etc.)
  • βœ… Context causes re-renders of all consumers when value changes
  • βœ… Use useCallback and useMemo to optimize context values
  • βœ… Prefer props for explicit dependencies
  • βœ… Use context when a prop would pass through many intermediate components

Ready to practice? Challenges | Next: State Performance Optimization


Resources

Python Docs

Ojasa Mirai

Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.

Learn Deep β€’ Build Real β€’ Verify Skills β€’ Launch Forward

Courses

PythonFastapiReactJSCloud

Β© 2026 Ojasa Mirai. All rights reserved.

TwitterGitHubLinkedIn