After five years building everything from e-commerce platforms to real-time trading systems, I've seen the same React patterns work across every scale—and the same mistakes repeated by developers who skip the fundamentals. The difference between code that scales and code that breaks comes down to one thing: Thinking in React. This isn't about hooks, performance optimization, or the latest framework feature. It's about developing a mental model that makes every other React concept automatic.
When you think in React, you stop fighting the framework and start working with it. State management becomes obvious. Component boundaries feel natural. Re-renders aren't a mystery. Let me walk you through the exact mental model I use on every project.
The Five Steps: Your Mental Model
React gave us a structured approach to building UIs, and it's still the best framework for organizing your thinking. These five steps form a checklist I follow on every feature—whether it's a button or a complete SaaS dashboard.
Step 1: Break the UI into Components
Start by looking at your design or wireframe and identify where the natural boundaries are. A component should do one thing. If you can name it simply and explain its purpose in one sentence, you're on the right track.
On a product listing page, you might have:
- ProductList (the container)
- ProductCard (individual item)
- FilterPanel (sidebar)
- SortDropdown (the sort control)
- PaginationButtons (bottom navigation)
Don't overthink this step. You can always refactor later. The goal is to see the tree structure—which components own which children.
Step 2: Build a Static Version First
This is the step most developers skip, and it costs them time later. Write the JSX, pass dummy data as props, get the layout and styling right—but don't touch state yet. No useState, no API calls, no interactivity.
export function ProductCard({ product }) {
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p className="price">${product.price}</p>
<button>Add to Cart</button>
</div>
);
}
export function ProductList({ products }) {
return (
<div className="product-list">
{products.map(p => (
<ProductCard key={p.id} product={p} />
))}
</div>
);
}This gives you three wins: you see the prop flow clearly, your team can review the structure before logic gets complex, and you catch design issues before writing state logic.
Step 3: Find the Minimal State
Now ask: what actually needs to change? What's the absolute minimum you need to track in state?
For a product listing with filters, you need:
- Selected filter values (category, price range)
- Current page number
- Maybe sort order
You do not need to store:
- The filtered product list (compute it from state + all products)
- Whether a filter panel is "open" if you can derive it from state
- Derived values like "product count" (compute it when you need it)
This is where the mental shift happens. In jQuery or vanilla JS, you'd store everything. In React, you store only the raw data that changes and compute everything else on render.
Step 4: Decide Where State Lives
Once you know what needs state, ask: who needs this data?
If only one component needs it—a toggle button's open state—keep it there. But if multiple components need the same data, it lives in their closest common parent.
In a product list where both FilterPanel and ProductList need to know the selected filters, the parent component owns it:
export function ProductPage() {
const [filters, setFilters] = useState({ category: null, priceRange: [0, 1000] });
const [sortBy, setSortBy] = useState("price");
const filteredProducts = useMemo(() => {
return allProducts.filter(p =>
(!filters.category || p.category === filters.category) &&
p.price >= filters.priceRange[0] &&
p.price <= filters.priceRange[1]
);
}, [filters]);
return (
<div className="product-page">
<FilterPanel filters={filters} onFiltersChange={setFilters} />
<ProductList products={filteredProducts} sortBy={sortBy} />
</div>
);
}Notice: FilterPanel and ProductList don't own the state. They receive it as props. This makes state flow predictable.
Step 5: Pass Data Down, Events Up
React is unidirectional. Data flows down as props. Changes flow up as callbacks.
FilterPanel doesn't call setFilters directly. It calls a callback it received as a prop:
export function FilterPanel({ filters, onFiltersChange }) {
const handleCategoryChange = (category) => {
onFiltersChange({ ...filters, category });
};
return (
<div className="filter-panel">
<h3>Category</h3>
<button onClick={() => handleCategoryChange("electronics")}>
Electronics
</button>
<button onClick={() => handleCategoryChange("clothing")}>
Clothing
</button>
</div>
);
}This pattern scales to any complexity. From ride-hailing to trading platforms, I use this same data-down, events-up flow.
UI is a Function of State
This is the mental shift that everything else builds on. Stop thinking "how do I update the DOM?" Start thinking "what state do I need, and how should the UI look given that state?"
In vanilla JavaScript, you'd do this:
// Don't do this in React
const button = document.querySelector(".like-button");
button.addEventListener("click", () => {
button.classList.add("liked");
button.textContent = "Unlike";
});
In React, you think about state first:
// Think like this
function LikeButton() {
const [isLiked, setIsLiked] = useState(false);
return (
<button
className={isLiked ? "liked" : ""}
onClick={() => setIsLiked(!isLiked)}
>
{isLiked ? "Unlike" : "Like"}
</button>
);
}
Notice the difference: React owns the render logic. You describe what the UI should be for every possible state, and React handles the DOM updates. You never manually touch DOM elements.
This pattern means:
- No bugs from forgetting to update the DOM
- No inconsistency between what's in state and what's displayed
- Easy to reason about: if state X is true, UI looks Y. Always.
I've shipped React Native apps with 200+ screens and .NET backends with millions of users. The scaling secret isn't complexity—it's keeping this rule absolute. UI always reflects state. Period.
Minimal State: Don't Store What You Can Compute
Here's where junior React developers often waste hours debugging. They store too much state.
Common mistake: storing derived data.
// ❌ Don't do this
function ShoppingCart({ items }) {
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((sum, item) => sum + item.price * item.quantity, 0));
}, [items]);
return <div>Total: ${total}</div>;
}
Why? If items changes and you forget to update total, they're out of sync. The component lies about state.
// ✅ Do this
function ShoppingCart({ items }) {
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
return <div>Total: ${total}</div>;
}
Same result, no sync bugs, cleaner code.
Another common mistake: storing booleans that derive from other state.
// ❌ Don't do this
const [user, setUser] = useState(null);
const [isLoggedIn, setIsLoggedIn] = useState(false);
// Now you have to keep these in sync. Error-prone.
// ✅ Do this
const [user, setUser] = useState(null);
const isLoggedIn = user !== null;
On a real CRM system I built, we had forms with dozens of fields. Engineers were storing "hasErrors" and "isValid" as state. When field values changed, these flags got stale. Switched to computing them from field values—bugs vanished.
Lift State to the Closest Common Parent
The question is never "where should this state live?" It's "who needs this state?"
If only ProductCard needs to know if it's favorited, state stays in ProductCard. If ProductList and a wishlist sidebar both need the same favorite list, that state moves to their common parent—Page or App.
Real example from an e-commerce platform I worked on:
// ❌ This leads to problems
function ProductCard({ productId }) {
const [isFavorited, setIsFavorited] = useState(false);
// ProductCard doesn't know if another ProductCard favorited the same item
}
function WishlistSidebar() {
const [favorites, setFavorites] = useState([]);
// WishlistSidebar has its own list, out of sync with ProductCards
}
// ✅ Better
function Shop() {
const [favorites, setFavorites] = useState([]);
const toggleFavorite = (productId) => {
setFavorites(prev =>
prev.includes(productId)
? prev.filter(id => id !== productId)
: [...prev, productId]
);
};
return (
<>
<ProductGrid
favorites={favorites}
onToggleFavorite={toggleFavorite}
/>
<WishlistSidebar favorites={favorites} />
</>
);
}
Now both ProductGrid and WishlistSidebar see the same truth. If you favorite something in the grid, the sidebar updates instantly. No sync bugs. No "why is my wishlist showing different items than the favorites I just clicked?"
As systems grow—I've worked on ride-hailing platforms with hundreds of components—this principle keeps state predictable. It's one reason Redux or context libraries exist: they formalize "lifting state" to a global level when needed.
Common Mistakes
Storing everything in state because it might change
Props that you pass down but never modify don't need state. A product's description won't change while rendering a ProductCard—it's just data you display.
Forgetting that state lives in a parent, not the child
New developers often write state inside child components and then wonder why changes don't sync with siblings. Remember: if two components need the same data, the parent owns it.
Deriving state inside render without useMemo
Computing derived data (like filtered lists) every render isn't wrong, but it's wasteful. Use useMemo when computation is expensive or when you need the reference to stay stable (like passing to child components).
Not separating UI state from data state
Whether a modal is open is UI state. The data inside the modal is data state. They might live in different places. A search input's text might be local state in a SearchBox component, but the actual search results live in a parent.
Key Takeaways
- Always follow the five steps: components → static version → minimal state → where state lives → data down, events up. This isn't optional; it's your map.
- Think "UI is a function of state," not "how do I manipulate the DOM?" Let React handle DOM updates while you describe what the UI should be.
- Store minimal state. If you can compute it, don't store it. Derived data in state is a bug factory.
- Lift state to the closest common parent. This is how you avoid sync bugs and make components reusable.
- Data flows down, events flow up. This unidirectional pattern is what makes React predictable at any scale.
Master this mental model and everything else—hooks, performance, testing, scaling—becomes straightforward. You're not fighting the framework anymore. You're working with it.






Comments
Loading comments…