The Secret Behind Every Web Feature, No Matter How Complex, Reduces to Three Core Mechanics
When I started out as a frontend developer, every new feature felt like a completely different mountain to climb. A like button felt nothing like a shopping cart. A comment section felt completely unrelated to a dark mode toggle. I kept thinking I had to learn a hundred distinct tricks just to survive. Then I realized something that changed everything. Every single feature on the web does the exact same three things: - Save the Data - Generate the HTML - Make it Interactive That's it. That's the whole trick. Instagram's feed, Amazon's cart, your bank's dashboard, this very blog post's comment section - strip away the frameworks, the animations, the design polish, and you're left with the same three-step loop, repeated over and over. In this post I'll break down what each step actually means, and then we'll prove it by building a small, clean e-commerce UI with React + Zustand, a product list and a shopping cart, step by step, mechanic by mechanic. Prerequisites You don't need to be advanced to follow along, but you'll get the most out of this if you already have: - Basic React knowledge - components, props, and useState. - Comfort with JavaScript ES6+ - arrow functions, array methods like .map() and .filter(), and the spread operator (...) show up throughout. - Node.js installed (v18+) so you can run the Vite dev server. - A code editor - VS Code is fine - and a few minutes to follow the terminal commands. No prior experience with Zustand is required. I'll cover exactly what you need to know as we go. The Three Mechanics 1. Save the Data Before anything appears on screen, it has to exist as raw information. A tweet is just an object with text and an author name. A cart is just an array of items with quantities. - A tweet is { text, author, likes, timestamp } - A product is { name, price, image, inStock } - A cart is [{ productId, quantity }] In a React app, "saving the data" usually means putting it into state - useState, useReducer, or a state management library like Zustand. The data doesn't care how it will be displayed. It's just the source of truth. 2. Generate the HTML Once you have data, the next job is to turn it into something the browser can render. In React, this is just a function: data goes in, JSX (which becomes HTML) comes out. products.map(product => ) Ten products in your data โ ten cards on the screen. Zero products โ an empty state. This step is purely mechanical: same data in, same HTML out, every time. That predictability is the whole point of a component-based framework like React. 3. Make it Interactive Static pages are boring. Interactivity simply means the user does something that changes the underlying data. - Click "Add to Cart" โ the cart data changes โ the cart HTML re-renders - Type in a search box โ the filter data changes โ the list HTML re-renders - Toggle dark mode โ a boolean changes โ the whole UI re-renders This is the loop: an event handler updates the data, and step 2 runs again automatically. React (and Zustand) exist almost entirely to make this loop fast, predictable, and easy to reason about. That's the whole secret. A "complex" feature is just this loop, nested and repeated many times with more data shapes involved. Once you see it, you can't unsee it and it becomes a lot easier to plan any feature, because you just ask yourself: what's the data, how do I render it, and what changes it? Let's build something with this exact mental model. What We're Building A small, clean e-commerce screen: - A product list (Save the Data โ Generate the HTML) - An "Add to Cart" button on each product (Make it Interactive) - A cart panel showing items, quantities, and a total, with the ability to remove items We'll use Zustand for state because it maps almost one-to-one onto "Save the Data" - no boilerplate, no reducers, no context providers. Just a store. Project setup npm create vite@latest mini-shop -- --template react choose Eslint cd mini-shop npm install npm install zustand npm run dev Your folder structure will look like this by the end: src/ data/ products.js store/ useCartStore.js components/ ProductCard.jsx ProductList.jsx Cart.jsx App.jsx App.css Step 1: Save the Data First, the raw product data. This is just an array, no framework magic yet. Create a folder inside your src directory named data, and create a file inside it(data) called products.js : Note: Follow this exact pattern for creating folders and files throughout the entire project. Create a folder inside your src directory named [folder-name], and create a file inside it called [file-name]: // src/data/products.js export const products = [ { id: 1, name: "Wireless Headphones", price: 59.99, image: "๐ง" }, { id: 2, name: "Mechanical Keyboard", price: 89.99, image: "โจ๏ธ" }, { id: 3, name: "Smart Watch", price: 129.99, image: "โ" }, { id: 4, name: "Desk Lamp", price: 24.99, image: "๐ก" }, { id: 5, name: "Bluetooth Speaker", price: 45.0, image: "๐" }, { id: 6, name: "Backpack", price: 39.99, image: "๐" }, ]; Now let's proceed to the part that matters most: the cart data, and the only place allowed to change it, which is our Zustand store. A Quick Note on Zustand Zustand is a small state management library for React. Where useState only lives inside one component, Zustand's store lives outside the component tree entirely , any component can read from it or update it, without prop-drilling or wrapping your app in a Context provider. The create function builds a hook - in our case, useCartStore. That hook is the store: - Call it with a selector, like useCartStore(state => state.items), and a component subscribes to just that slice of data. It only re-renders when items actually changes. - Call any function you defined inside the store, like addToCart(product), and it updates the store's state directly - no dispatching actions, no reducers. That's really it. Compared to useState, it trades "state that belongs to one component" for "state that belongs to the app," which is exactly what a cart needs - the ProductCard that adds an item and the Cart panel that displays it are two completely different components, and Zustand is the shared source of truth connecting them. // src/store/useCartStore.js import { create } from "zustand"; const useCartStore = create((set, get) => ({ items: [], // [{ id, name, price, image, quantity }] addToCart: (product) => { const existing = get().items.find((item) => item.id === product.id); if (existing) { set({ items: get().items.map((item) => item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item ), }); } else { set({ items: [...get().items, { ...product, quantity: 1 }] }); } }, removeFromCart: (id) => { set({ items: get().items.filter((item) => item.id !== id) }); }, totalPrice: () => get().items.reduce((sum, item) => sum + item.price * item.quantity, 0), totalItems: () => get().items.reduce((sum, item) => sum + item.quantity, 0), })); export default useCartStore; Notice: this file has zero HTML. It doesn't know or care what a "cart" looks like on screen. It only knows how to hold and change data. That separation is the whole reason step 1 exists on its own - your data logic should survive a complete redesign of your UI untouched. Step 2: Generate the HTML Now we turn that data into components. Each component is a pure translation: data in, markup out. // src/components/ProductCard.jsx import useCartStore from "../store/useCartStore"; function ProductCard({ product }) { const addToCart = useCartStore((state) => state.addToCart); return ( {product.image} {product.name} ${product.price.toFixed(2)} addToCart(product)}>Add to Cart ); } export default ProductCard; // src/components/ProductList.jsx import { products } from "../data/products"; import ProductCard from "./ProductCard"; function ProductList() { return ( {products.map((product) => ( ))} ); } export default ProductList; At this point, if you render in your App.jsx, you'll see six product cards on screen - pure step 2, no interactivity wired up beyond the click handler we're about to explain. Now the cart's HTML - again, just a translation of items into markup: // src/components/Cart.jsx import useCartStore from "../store/useCartStore"; function Cart() { const items = useCartStore((state) => state.items); const removeFromCart = useCartStore((state) => state.removeFromCart); const totalPrice = useCartStore((state) => state.totalPrice()); if (items.length === 0) { return ( Your Cart Your cart is empty. ); } return ( Your Cart {items.map((item) => ( {item.image} {item.name} {item.quantity} ร ${item.price.toFixed(2)} removeFromCart(item.id)} > โ ))} Total: ${totalPrice.toFixed(2)} ); } export default Cart; Step 3: Make It Interactive Here's the thing worth pausing on: we already wrote the interactivity. It happened the moment we called addToCart ** and **removeFromCart inside onClick. addToCart(product)}>Add to Cart Trace the loop: - - User clicks โ addToCart(product) runs - - The Zustand store's items array changes (Save the Data, step 1, happening again) - - Every component subscribed to items - in this case, Cart - automatically re-renders (Generate the HTML, step 2, happening again) There's no manual DOM manipulation, no "find the cart element and update its innerHTML." React does that reconciliation for you. All we had to write was the part that changes the data. That's the entire secret of interactivity: you never update the screen directly - you update the data, and let step 2 run again. Let's wire it all together: // src/App.jsx import ProductList from "./components/ProductList"; import Cart from "./components/Cart"; import "./App.css"; function App() { return ( Mini Shop ); } export default App; Making It Look Nice Let's add a small stylesheet to give this the feel of a real product, without any UI library: src/App.css * { box-sizing: border-box; } body { margin: 0; font-family: "Segoe UI", system-ui, sans-serif; background: #f6f7fb; color: #1a1a1a; } .app-header { background: #111827; color: white; padding: 1.25rem
Comments
No comments yet. Start the discussion.