
ReactJS
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 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>
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 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>
);
}ββββββββββββββββ¬βββββββββββββββ¬βββββββββββββββ¬βββββββββββββββ
β 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 β β β β
ββββββββββββββββ΄βββββββββββββββ΄βββββββββββββββ΄βββββββββββββββReady to practice? Challenges | Next: State Performance Optimization
Resources
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