<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[How to Design a Scalable Frontend Architecture (From Scratch)]]></title><description><![CDATA[How to Design a Scalable Frontend Architecture (From Scratch)]]></description><link>https://blog.kunalgoel.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/69c222b930a9b81e3afd3695/04ef452c-f7ee-490a-aa09-c86b266150f4.jpg</url><title>How to Design a Scalable Frontend Architecture (From Scratch)</title><link>https://blog.kunalgoel.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 13 May 2026 12:12:40 GMT</lastBuildDate><atom:link href="https://blog.kunalgoel.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Performance Patterns That Actually Matter in Production]]></title><description><![CDATA[We're talking performance today.
And before you scroll away thinking "yeah yeah, lazy load your images, use a CDN" — I promise this is not that blog. We're going deeper. We're talking about the stuff ]]></description><link>https://blog.kunalgoel.dev/performance-patterns-that-actually-matter-in-production</link><guid isPermaLink="true">https://blog.kunalgoel.dev/performance-patterns-that-actually-matter-in-production</guid><dc:creator><![CDATA[kunal goel]]></dc:creator><pubDate>Tue, 12 May 2026 15:34:39 GMT</pubDate><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/69c222b930a9b81e3afd3695/accca841-baa0-44db-a4c3-0269c740e47c.png" alt="" style="display:block;margin:0 auto" />

<p>We're talking performance today.</p>
<p>And before you scroll away thinking "yeah yeah, lazy load your images, use a CDN" — I promise this is not that blog. We're going deeper. We're talking about the stuff that actually comes up in system design interviews AND the stuff that makes your production app not feel like garbage.</p>
<p>Let's get into it.</p>
<hr />
<h2>Why Performance Even Matters (Beyond the Obvious)</h2>
<p>Here's the real talk: a 1 second delay in page load = ~7% drop in conversions. Google uses Core Web Vitals as a ranking signal. And your users on mobile in tier-2 cities? They're on 4G with 200ms latency. If your app isn't optimized, they're gone.</p>
<p>But in interviews, performance is also where juniors and seniors get separated. A junior says "use lazy loading." A senior says "here's <em>when</em> to use it, <em>why</em>, and what the tradeoff is." That's what we're building today.</p>
<hr />
<h2>The Four Pillars We're Covering Today</h2>
<ol>
<li><p>Virtualization</p>
</li>
<li><p>Code Splitting</p>
</li>
<li><p>Lazy Loading</p>
</li>
<li><p>Bundle Size Optimization</p>
</li>
</ol>
<p>Each one has a <em>when to use it</em>, a <em>how it works</em>, and a <em>real example</em>. Let's go.</p>
<hr />
<h2>1. Virtualization — Don't Render What You Can't See</h2>
<p>Imagine you have a list of 10,000 rows in a table. If you render all 10,000 DOM nodes at once, your browser is sweating. FPS drops. Scrolling feels janky. Users hate it.</p>
<p><strong>Virtualization</strong> (also called windowing) solves this by only rendering the rows currently visible in the viewport — plus a small buffer above and below.</p>
<h3>How it works</h3>
<p>You maintain a virtual list. As the user scrolls, you calculate which items should be visible based on scroll position and item height, and only render those. Items that scroll out of view get unmounted (or recycled).</p>
<pre><code class="language-jsx">// Without virtualization — renders all 10,000 rows
function BadList({ items }) {
  return (
    &lt;div&gt;
      {items.map(item =&gt; &lt;Row key={item.id} data={item} /&gt;)}
    &lt;/div&gt;
  );
}

// With react-window — only renders ~20 visible rows
import { FixedSizeList } from 'react-window';

function GoodList({ items }) {
  return (
    &lt;FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    &gt;
      {({ index, style }) =&gt; (
        &lt;div style={style}&gt;
          &lt;Row data={items[index]} /&gt;
        &lt;/div&gt;
      )}
    &lt;/FixedSizeList&gt;
  );
}
</code></pre>
<h3>When to use it</h3>
<ul>
<li><p>Lists with 100+ items that users scroll through</p>
</li>
<li><p>Data tables, feeds, chat history, autocomplete dropdowns with many results</p>
</li>
</ul>
<h3>The tradeoff</h3>
<p>Virtualization adds complexity. For small lists (under 50 items), it's overkill. Don't over-engineer.</p>
<hr />
<h2>2. Code Splitting — Don't Ship What You Don't Need Yet</h2>
<p>By default, a React app bundles everything into one giant JS file. User lands on your home page? They're downloading code for your settings page, your admin dashboard, and that feature you shipped last quarter that nobody uses.</p>
<p><strong>Code splitting</strong> breaks your bundle into smaller chunks that load on demand.</p>
<h3>How it works in React</h3>
<p>React has <code>React.lazy</code> and <code>Suspense</code> built-in for this.</p>
<pre><code class="language-jsx">import React, { Suspense, lazy } from 'react';

// Before — eagerly loaded, always in bundle
import HeavyDashboard from './HeavyDashboard';

// After — lazily loaded, separate chunk
const HeavyDashboard = lazy(() =&gt; import('./HeavyDashboard'));

function App() {
  return (
    &lt;Suspense fallback={&lt;div&gt;Loading...&lt;/div&gt;}&gt;
      &lt;HeavyDashboard /&gt;
    &lt;/Suspense&gt;
  );
}
</code></pre>
<p>Vite and webpack both handle the actual chunk splitting automatically once you use dynamic imports.</p>
<h3>Route-based splitting — the most common pattern</h3>
<pre><code class="language-jsx">import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

const Home = lazy(() =&gt; import('./pages/Home'));
const Dashboard = lazy(() =&gt; import('./pages/Dashboard'));
const Settings = lazy(() =&gt; import('./pages/Settings'));

function App() {
  return (
    &lt;Suspense fallback={&lt;PageLoader /&gt;}&gt;
      &lt;Routes&gt;
        &lt;Route path="/" element={&lt;Home /&gt;} /&gt;
        &lt;Route path="/dashboard" element={&lt;Dashboard /&gt;} /&gt;
        &lt;Route path="/settings" element={&lt;Settings /&gt;} /&gt;
      &lt;/Routes&gt;
    &lt;/Suspense&gt;
  );
}
</code></pre>
<p>Now each route is its own chunk. User never visits settings? Settings code never downloads.</p>
<h3>When to use it</h3>
<ul>
<li><p>Route-level splitting: always, almost no downside</p>
</li>
<li><p>Component-level: heavy components like rich text editors, charts, map libraries</p>
</li>
<li><p>Feature flags: code behind a flag shouldn't be in the main bundle</p>
</li>
</ul>
<hr />
<h2>3. Lazy Loading — Images and Components on Demand</h2>
<p>We covered component lazy loading above. But images are actually where lazy loading has the biggest impact for most apps.</p>
<h3>Native lazy loading (just use this)</h3>
<pre><code class="language-html">&lt;img src="product.jpg" alt="Product" loading="lazy" /&gt;
</code></pre>
<p>That's it. The browser handles it. It defers loading images that are below the fold until the user scrolls near them. Zero JS required.</p>
<h3>Intersection Observer — when you need control</h3>
<p>For more custom behaviour (like infinite scroll, or lazy loading non-image content), use the Intersection Observer API.</p>
<pre><code class="language-jsx">import { useEffect, useRef, useState } from 'react';

function LazyImage({ src, alt }) {
  const imgRef = useRef(null);
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() =&gt; {
    const observer = new IntersectionObserver(
      ([entry]) =&gt; {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.disconnect(); // stop observing once loaded
        }
      },
      { threshold: 0.1 }
    );

    if (imgRef.current) observer.observe(imgRef.current);
    return () =&gt; observer.disconnect();
  }, []);

  return (
    &lt;div ref={imgRef}&gt;
      {isVisible
        ? &lt;img src={src} alt={alt} /&gt;
        : &lt;div className="placeholder" /&gt;
      }
    &lt;/div&gt;
  );
}
</code></pre>
<h3>The threshold option</h3>
<p><code>threshold: 0.1</code> means "trigger when 10% of the element is visible." Set it higher (0.5) for elements you want mostly in view before loading.</p>
<hr />
<h2>4. Bundle Size — The Invisible Performance Tax</h2>
<p>Your bundle size is a silent killer. Every kb of JS has to be:</p>
<ul>
<li><p>Downloaded over the network</p>
</li>
<li><p>Parsed by the browser</p>
</li>
<li><p>Compiled and executed</p>
</li>
</ul>
<p>A 500kb bundle vs a 200kb bundle isn't just a download difference — the parse and execution time on a mid-range Android phone can differ by seconds.</p>
<h3>Check your bundle first</h3>
<p>Don't optimise blindly. Use <code>rollup-plugin-visualizer</code> with Vite:</p>
<pre><code class="language-javascript">// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';

export default {
  plugins: [
    visualizer({ open: true }) // opens a treemap of your bundle after build
  ]
}
</code></pre>
<p>Run <code>npm run build</code> and it opens a visual breakdown. You'll probably find one library eating 40% of your bundle.</p>
<h3>Common culprits and fixes</h3>
<p><strong>Moment.js</strong> — 67kb gzipped just for date formatting. Replace with <code>date-fns</code> (tree-shakeable, you only import what you use) or <code>dayjs</code> (2kb).</p>
<pre><code class="language-javascript">// Bad — imports entire moment library
import moment from 'moment';

// Good — tree-shakeable, only imports format
import { format } from 'date-fns';
</code></pre>
<p><strong>Lodash</strong> — don't import the whole thing.</p>
<pre><code class="language-javascript">// Bad — imports entire lodash
import _ from 'lodash';
_.debounce(fn, 300);

// Good — only imports debounce
import debounce from 'lodash/debounce';
</code></pre>
<p><strong>Icon libraries</strong> — importing from <code>@mui/icons-material</code> wrongly can pull in thousands of icons.</p>
<pre><code class="language-javascript">// Bad — might import entire icon set depending on bundler config
import { Delete } from '@mui/icons-material';

// Good — direct import, guaranteed single icon
import DeleteIcon from '@mui/icons-material/Delete';
</code></pre>
<h3>Tree shaking — make sure it's working</h3>
<p>Tree shaking removes unused exports from your bundle. It only works with ES modules (<code>import/export</code>). If a library uses CommonJS (<code>require</code>), it can't be tree-shaken.</p>
<p>Check package.json of your dependency for <code>"module"</code> or <code>"exports"</code> field — that's a sign it supports ESM and tree shaking.</p>
<hr />
<h2>Putting It All Together — Interview Answer Framework</h2>
<p>When a system design interviewer asks about performance, don't just list techniques. Structure your answer:</p>
<p><strong>1. Identify the bottleneck first</strong></p>
<ul>
<li><p>Is it initial load time? → Code splitting, bundle size</p>
</li>
<li><p>Is it runtime rendering? → Virtualization, memoization</p>
</li>
<li><p>Is it network? → Lazy loading, caching, CDN</p>
</li>
</ul>
<p><strong>2. Propose the solution with the tradeoff</strong></p>
<ul>
<li>"I'd use virtualization here because we have 10k rows — the tradeoff is added complexity but the rendering performance gain is worth it."</li>
</ul>
<p><strong>3. Mention metrics</strong></p>
<ul>
<li><p>LCP (Largest Contentful Paint) — loading</p>
</li>
<li><p>FID/INP (Interaction to Next Paint) — interactivity</p>
</li>
<li><p>CLS (Cumulative Layout Shift) — visual stability</p>
</li>
</ul>
<p>Saying "I'd measure this with Lighthouse and set a performance budget" puts you ahead of 90% of candidates.</p>
<hr />
<h2>Quick Summary</h2>
<table>
<thead>
<tr>
<th>Pattern</th>
<th>Problem it solves</th>
<th>When to use</th>
</tr>
</thead>
<tbody><tr>
<td>Virtualization</td>
<td>Too many DOM nodes</td>
<td>Lists 100+ items</td>
</tr>
<tr>
<td>Code Splitting</td>
<td>Oversized initial bundle</td>
<td>Route/feature level</td>
</tr>
<tr>
<td>Lazy Loading</td>
<td>Loading unused resources</td>
<td>Images, heavy components</td>
</tr>
<tr>
<td>Bundle optimization</td>
<td>Bloated dependencies</td>
<td>Always, check with visualizer</td>
</tr>
</tbody></table>
<hr />
<h2>What's Next</h2>
<p>Day 5 we're going into <strong>state management patterns</strong> — when to use local state vs context vs Redux vs Zustand, and how to think about this in a system design interview without just saying "it depends."</p>
<p>If this helped, drop a reaction or share it with someone grinding frontend interviews. And if you disagree with anything here — good, argue with me in the comments. That's how we both learn.</p>
]]></content:encoded></item><item><title><![CDATA[Component Design Patterns Every React Dev Should Know (Compound Components, Render Props & Custom Hooks)]]></title><description><![CDATA[First, a decision diagram — the question you should ask before picking a pattern:


I used to build components in one big blob. Everything in one file — state, logic, markup, API calls. It worked. Unt]]></description><link>https://blog.kunalgoel.dev/component-design-patterns-every-react-dev-should-know-compound-components-render-props-custom-hooks</link><guid isPermaLink="true">https://blog.kunalgoel.dev/component-design-patterns-every-react-dev-should-know-compound-components-render-props-custom-hooks</guid><dc:creator><![CDATA[kunal goel]]></dc:creator><pubDate>Thu, 26 Mar 2026 08:53:23 GMT</pubDate><content:encoded><![CDATA[<p>First, a decision diagram — the question you should ask before picking a pattern:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69c222b930a9b81e3afd3695/15395a57-e26e-460d-92e8-1234356e541f.png" alt="" style="display:block;margin:0 auto" />

<p>I used to build components in one big blob. Everything in one file — state, logic, markup, API calls. It worked. Until someone asked me to reuse just <em>part</em> of it somewhere else, and I realised I had no idea how to do that cleanly.</p>
<p>That's what component design patterns solve. They're not fancy abstractions for the sake of it — they're answers to real problems you will hit as your codebase grows. And they come up in frontend system design interviews all the time, so worth knowing properly.</p>
<p>Three patterns I think every React dev should have in their toolkit: custom hooks, compound components, and render props. Let's go through each one with real examples.</p>
<h3>Pattern 2 — <strong>Custom hooks</strong></h3>
<p><strong>The problem:</strong> You have the same stateful logic copy-pasted across 5 components. A fetch call, a debounce, a form handler. Every time you fix a bug in one, you forget to fix the other four.</p>
<p><strong>The solution:</strong> Extract it into a custom hook.</p>
<pre><code class="language-tsx">// hooks/useFetch.ts
function useFetch&lt;T&gt;(url: string) {
  const [data, setData] = useState&lt;T | null&gt;(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState&lt;Error | null&gt;(null);

  useEffect(() =&gt; {
    fetch(url)
      .then(res =&gt; res.json())
      .then(setData)
      .catch(setError)
      .finally(() =&gt; setLoading(false));
  }, [url]);

  return { data, loading, error };
}
</code></pre>
<p>Now any component can just:</p>
<pre><code class="language-tsx">const { data, loading, error } = useFetch&lt;User[]&gt;('/api/users');
</code></pre>
<p>The component has no idea how fetching works. It just gets the result. Change the fetch logic once in <code>useFetch</code> and every component using it gets the fix automatically.</p>
<p><strong>Rule of thumb:</strong> If you're copying stateful logic (anything using <code>useState</code> or <code>useEffect</code>) between components, that's a custom hook waiting to be extracted.</p>
<h3>Pattern 2 — Compound components</h3>
<p><strong>The problem:</strong> You're building a <code>&lt;Tabs&gt;</code> component. You want the API to feel natural and flexible — consumers should be able to control what tabs show up, what order they're in, maybe add custom content between them. But you also don't want to pass 15 props into one giant component.</p>
<p><strong>The solution:</strong> Compound components — a parent that owns shared state, and child components that consume it via context.</p>
<pre><code class="language-tsx">// The parent holds state and provides context
const TabsContext = createContext(null);

function Tabs({ children }) {
  const [active, setActive] = useState(0);
  return (
    &lt;TabsContext.Provider value={{ active, setActive }}&gt;
      &lt;div&gt;{children}&lt;/div&gt;
    &lt;/TabsContext.Provider&gt;
  );
}

// Children consume the context
function Tab({ index, children }) {
  const { active, setActive } = useContext(TabsContext);
  return (
    &lt;button
      onClick={() =&gt; setActive(index)}
      style={{ fontWeight: active === index ? 600 : 400 }}
    &gt;
      {children}
    &lt;/button&gt;
  );
}

function TabPanel({ index, children }) {
  const { active } = useContext(TabsContext);
  return active === index ? &lt;div&gt;{children}&lt;/div&gt; : null;
}

// Attach children as properties (optional but common)
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;
</code></pre>
<p>Usage feels completely natural:</p>
<pre><code class="language-tsx">&lt;Tabs&gt;
  &lt;Tabs.Tab index={0}&gt;Overview&lt;/Tabs.Tab&gt;
  &lt;Tabs.Tab index={1}&gt;Settings&lt;/Tabs.Tab&gt;

  &lt;Tabs.Panel index={0}&gt;Overview content here&lt;/Tabs.Panel&gt;
  &lt;Tabs.Panel index={1}&gt;Settings content here&lt;/Tabs.Panel&gt;
&lt;/Tabs&gt;
</code></pre>
<p>You've seen this pattern in the wild — Radix UI, Headless UI, Reach UI all use it heavily. It's why their component APIs feel so clean.</p>
<p><strong>Rule of thumb:</strong> If you're building a UI component where the consumer should control structure and composition, reach for compound components.</p>
<h3>Pattern 3 — Render props</h3>
<p><strong>The problem:</strong> You have logic that produces some data or state, and you want to let the consumer decide how to render it. Not just <em>what</em> data to show, but <em>how</em> to show it — completely different UI in different contexts.</p>
<p><strong>The solution:</strong> Render props — pass a function as a prop, call it with the data, let the consumer return whatever JSX they want.</p>
<pre><code class="language-tsx">function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  return (
    &lt;div onMouseMove={e =&gt; setPosition({ x: e.clientX, y: e.clientY })}&gt;
      {render(position)}
    &lt;/div&gt;
  );
}
</code></pre>
<p>Consumer decides the UI completely:</p>
<pre><code class="language-tsx">&lt;MouseTracker render={({ x, y }) =&gt; (
  &lt;p&gt;Mouse is at {x}, {y}&lt;/p&gt;
)} /&gt;

// Or somewhere else, totally different UI:
&lt;MouseTracker render={({ x, y }) =&gt; (
  &lt;Cursor style={{ left: x, top: y }} /&gt;
)} /&gt;
</code></pre>
<p>Same logic, completely different rendering. That's the power of render props.</p>
<p>These days, custom hooks have largely replaced render props because hooks are cleaner. But render props still show up in older codebases and in some specific scenarios — like when you need the rendered output to be tightly coupled to DOM events. Worth knowing.</p>
<p><strong>Rule of thumb:</strong> If you need to share logic where the consumer needs full control over what gets rendered, render props (or a custom hook returning data) work well.</p>
<h3>When to use which</h3>
<table>
<thead>
<tr>
<th>Scenario</th>
<th>Pattern</th>
</tr>
</thead>
<tbody><tr>
<td>Sharing fetch/form/timer logic</td>
<td>Custom hook</td>
</tr>
<tr>
<td>Building flexible UI components (Tabs, Accordion, Select)</td>
<td>Compound components</td>
</tr>
<tr>
<td>Sharing logic where rendering differs completely</td>
<td>Render props / hook</td>
</tr>
<tr>
<td>Just need data from a hook rendered differently</td>
<td>Custom hook returning state</td>
</tr>
</tbody></table>
<p>The honest answer is — most of the time, you'll reach for custom hooks. They're the cleanest, most composable, and easiest to test. Compound components are your go-to for complex UI components. Render props are worth understanding for interviews and legacy code.</p>
<hr />
<h3>Why this matters for system design interviews</h3>
<p>When you're designing a component library or a design system in an interview, the interviewer wants to see that you think about <em>API design</em> — not just "does it work" but "is it easy to use, flexible, and maintainable."</p>
<p>Compound components show you've thought about the consumer's experience. Custom hooks show you separate logic from UI. These signals require senior-level thinking.</p>
<h3>What's next</h3>
<p>Day 4 is going into <strong>performance patterns</strong> — virtualization, code splitting, lazy loading, and how to think about bundle size. Stuff that actually moves the needle in production apps.</p>
<p>If you've used any of these patterns in production and run into edge cases, drop them in the comments — I'm genuinely curious what breaks in the real world.</p>
]]></content:encoded></item><item><title><![CDATA[How React Actually Renders (and Why Your App Re-renders Too Much)]]></title><description><![CDATA[I spent two days last week staring at the React Profiler, wondering why a simple filter dropdown was causing 47 components to re-render. Forty-seven. For a dropdown.
That's when I stopped just using R]]></description><link>https://blog.kunalgoel.dev/how-react-actually-renders-and-why-your-app-re-renders-too-much</link><guid isPermaLink="true">https://blog.kunalgoel.dev/how-react-actually-renders-and-why-your-app-re-renders-too-much</guid><dc:creator><![CDATA[kunal goel]]></dc:creator><pubDate>Wed, 25 Mar 2026 06:20:04 GMT</pubDate><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/69c222b930a9b81e3afd3695/328c5e64-2e8b-4cf7-93cf-e3e10072e493.png" alt="" style="display:block;margin:0 auto" />

<p>I spent two days last week staring at the React Profiler, wondering why a simple filter dropdown was causing 47 components to re-render. Forty-seven. For a dropdown.</p>
<p>That's when I stopped just <em>using</em> React and started actually understanding how it works under the hood. Turns out, a lot of the "mysterious" performance issues make complete sense once you get the rendering model. Let's break it down.</p>
<p><strong>First — what even is a "render"?</strong></p>
<p>In React, a render is just React calling your component function. That's it. When React renders <code>&lt;UserCard /&gt;</code>, it literally runs the <code>UserCard</code> function and looks at what JSX came out.</p>
<p>People think rendering = DOM update. It doesn't. Rendering is React running your function. Updating the DOM is a separate step that only happens if something actually changed.</p>
<p>This distinction matters a lot.</p>
<p><strong>What triggers a re-render?</strong></p>
<p>Three things cause React to re-render a component:</p>
<ol>
<li><p><strong>State changes</strong> — you call setState or dispatch to a reducer. React re-renders that component and everything below it in the tree.</p>
</li>
<li><p><strong>Props changes</strong> — when a parent re-renders, all its children re-render too. Even if the child's props didn't change. Yes, really.</p>
</li>
<li><p><strong>Context changes</strong> — if a useContext value updates, every component consuming that context re-renders. The diagram above shows the full flow. Notice there's no "props actually changed" check by default — React just re-renders the whole subtree. This is where most performance issues come from.</p>
</li>
</ol>
<p><strong>The render, reconcile, commit cycle</strong></p>
<p>Once React decides to render, here's what happens:</p>
<p><strong>Render phase</strong> — React calls your component function and builds a new virtual DOM tree. This is pure JavaScript, nothing touches the browser yet.</p>
<p><strong>Reconciliation</strong> — React compares the new virtual DOM with the previous one. This is the "diffing" step. React figures out the minimum set of changes needed.</p>
<p><strong>Commit phase</strong> — React applies those changes to the actual DOM. This is the expensive step, but React tries to minimize it.</p>
<p>The key insight: just because React <em>renders</em> your component doesn't mean it <em>updates</em> the DOM. If nothing visually changed, the commit phase does nothing. Renders are cheap. DOM updates are expensive.</p>
<p><strong>The re-render problem in real life</strong></p>
<p>Here's a pattern I see everywhere — and had in my own code:</p>
<pre><code class="language-tsx">function Parent() {
  const [count, setCount] = useState(0);

  return (
    &lt;div&gt;
      &lt;button onClick={() =&gt; setCount(c =&gt; c + 1)}&gt;Click me&lt;/button&gt;
      &lt;ExpensiveChild /&gt;  {/* re-renders every time count changes */}
    &lt;/div&gt;
  );
}
</code></pre>
<p>Every time you click the button, <code>ExpensiveChild</code> re-renders — even though it has nothing to do with <code>count</code>. React doesn't know that. It just sees "parent rendered, re-render all children."</p>
<h3>Fix 1 — <code>React.memo</code></h3>
<p>Wrap the child in <code>React.memo</code> and React will skip re-rendering it if its props haven't changed:</p>
<p>tsx</p>
<pre><code class="language-tsx">const ExpensiveChild = React.memo(() =&gt; {
  return &lt;div&gt;I only render when my props change&lt;/div&gt;;
});
</code></pre>
<p>Now clicking the button won't touch <code>ExpensiveChild</code> at all. But there's a catch — if you're passing a function or object as a prop, this breaks down fast.</p>
<h3>Fix 2 — <code>useCallback</code> for stable function references</h3>
<p>tsx</p>
<pre><code class="language-tsx">function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() =&gt; {
    console.log('clicked');
  }, []); // stable reference across renders

  return &lt;ExpensiveChild onClick={handleClick} /&gt;;
}
</code></pre>
<p>Without <code>useCallback</code>, <code>handleClick</code> is a brand new function object on every render. <code>React.memo</code> sees a new prop value and re-renders anyway. <code>useCallback</code> gives you the same function reference between renders so the memo check actually works.</p>
<h3>Fix 3 — <code>useMemo</code> for expensive calculations</h3>
<p>tsx</p>
<pre><code class="language-tsx">const filteredList = useMemo(() =&gt; {
  return hugeList.filter(item =&gt; item.active);
}, [hugeList]);
</code></pre>
<p>Don't run this on every render. Memoize it and only recalculate when <code>hugeList</code> actually changes.</p>
<h3>When NOT to memoize</h3>
<p>This is the part nobody talks about — <strong>memoization has a cost too.</strong></p>
<p>React has to store the previous value, run the comparison, and decide whether to use the cached result. For simple components and cheap calculations, this overhead is often <em>more</em> expensive than just re-rendering.</p>
<p>My rule of thumb:</p>
<ul>
<li><p><code>React.memo</code> → only for components that render often AND are expensive to render</p>
</li>
<li><p><code>useMemo</code> → only for genuinely expensive calculations (think filtering/sorting 1000+ items)</p>
</li>
<li><p><code>useCallback</code> → mostly when passing callbacks to memoized children or as <code>useEffect</code> deps</p>
</li>
</ul>
<p>Don't sprinkle these everywhere <strong>"just in case." Profile first, optimize second.</strong></p>
<h3>How to actually find the problem — React Profiler</h3>
<p>Stop guessing. Open React DevTools, go to the Profiler tab, hit record, interact with your app, and stop recording.</p>
<p>You'll see a flamegraph showing exactly which components rendered and how long each one took. The bars that are wide and re-render repeatedly are your targets.</p>
<p>I found my 47-component re-render problem in about 3 minutes with the Profiler. Turned out a context value was being recreated on every render because the object wasn't memoized. One <code>useMemo</code> fixed it.</p>
<h3>The mental model to remember</h3>
<p>React rendering is a <strong>pull</strong> system, not a push system. When state changes, React doesn't surgically update just the affected component — it re-runs the entire subtree from that point downward, then figures out what actually needs to change in the DOM.</p>
<p>Your job as a developer is to give React hints — via <code>memo</code>, <code>useCallback</code>, <code>useMemo</code> — about what <em>hasn't</em> changed so it can skip work.</p>
<p>But don't optimize blindly. Profile first, find the actual bottleneck, then apply the right fix.</p>
<h3>What's next</h3>
<p>Day 3 is going to be on <strong>component design patterns</strong> — specifically compound components, render props, and custom hooks, and when to reach for each one. These patterns come up a lot in system design interviews so worth understanding properly.</p>
<p>Drop a comment if the Profiler thing clicked for you — or if you've had a wild re-render bug I should know about.</p>
]]></content:encoded></item><item><title><![CDATA[How to Design a Scalable Frontend Architecture (From Scratch)]]></title><description><![CDATA[Day 1 — Designing Scalable Frontend Architecture
When I first started building frontend apps, everything was fine. Components worked, state was managed, API calls happened. Then the app grew. And one ]]></description><link>https://blog.kunalgoel.dev/how-to-design-a-scalable-frontend-architecture-from-scratch</link><guid isPermaLink="true">https://blog.kunalgoel.dev/how-to-design-a-scalable-frontend-architecture-from-scratch</guid><dc:creator><![CDATA[kunal goel]]></dc:creator><pubDate>Tue, 24 Mar 2026 06:18:50 GMT</pubDate><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/69c222b930a9b81e3afd3695/ec2affca-667e-470c-ad0c-fa66647937ae.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Day 1 — Designing Scalable Frontend Architecture</strong></p>
<p>When I first started building frontend apps, everything was fine. Components worked, state was managed, API calls happened. Then the app grew. And one day I opened the codebase and thought — what the hell is going on here.</p>
<p>Components were 400 lines long. State was scattered everywhere. Three different places were calling the same API. Debugging meant spelunking through files with no idea where something lived.</p>
<p>That's when it clicked for me — <strong>frontend architecture isn't just about writing clean code, it's about designing a system that doesn't collapse under its own weight.</strong></p>
<p>This is blog #1 of my series documenting what I'm learning from <a href="https://www.greatfrontend.com/">GreatFrontend</a>. Let's get into it.</p>
<p><strong>What even is frontend architecture?</strong></p>
<p>It's the set of decisions you make before writing a single component — how you'll organize code, manage state, handle data fetching, and structure folders — so that when the app grows, it stays maintainable.</p>
<p>The goal isn't perfection. It's making sure that six months later, a new dev (or future-you) can open the codebase and not immediately want to quit.</p>
<p>Good frontend architecture is:</p>
<p><strong>Scalable</strong> — adding features doesn't require rewriting existing ones</p>
<p><strong>Maintainable</strong> — bugs are easy to find and fix</p>
<p><strong>Collaborative</strong> — multiple people can work without constantly stepping on each other</p>
<p><strong>Step 1: Think in layers</strong></p>
<p>The single most useful mental model I've internalized is: separate your concerns into layers. Every frontend app can be broken into three:</p>
<p>The diagram above shows how these layers talk to each other — and more importantly, what they don't do. The UI layer never calls an API directly. That's the rule.</p>
<p>The moment you call <code>fetch('/api/users')</code> inside a component, you've created a hidden dependency. Change the API? Now you're hunting through 12 components. Put it in the data layer, and you change it in one place.</p>
<p><strong>Step 2: Folder structure that doesn't fight you</strong></p>
<p>Most beginner projects look like this:</p>
<pre><code class="language-plaintext">components/
utils/
pages/
</code></pre>
<p>It works for a week. Then components/ has 60 files and you spend more time finding things than building them.</p>
<p>The better approach is <strong>feature-based architecture</strong>:</p>
<pre><code class="language-plaintext">features/
  auth/
    components/    ← auth-specific UI
    hooks/         ← useLogin, useLogout
    services/      ← authService.ts
  cart/
    components/
    hooks/
    services/
shared/
  ui/              ← Button, Modal, Input (reusable anywhere)
  utils/
  constants/
</code></pre>
<p>Everything related to auth lives in <code>features/auth/</code>. When a new dev joins and needs to touch auth logic, they know exactly where to look. When you delete a feature, you delete one folder.</p>
<p>The <code>shared/</code> folder is for stuff that genuinely has no business logic — pure UI components, utility functions, constants. If it's in <code>shared/</code>, it should have zero knowledge of your app's domain.</p>
<p><strong>Step 3: State management — don't overthink it</strong></p>
<p>I see a lot of people reach for Redux on day one. You don't need it. Use the simplest thing that works, and upgrade when you actually feel the pain.</p>
<p>Here's the decision tree I use:</p>
<ul>
<li><p><code>useState</code> → local component UI state (is this dropdown open? what's in this input field?)</p>
</li>
<li><p><strong>Context / Zustand</strong> → state shared across multiple components (user auth, theme, cart)</p>
</li>
<li><p><strong>React Query / TanStack Query</strong> → anything that comes from a server</p>
</li>
</ul>
<p>Concrete example — an e-commerce app:</p>
<ul>
<li><p>Cart items → Zustand (global, client-owned state)</p>
</li>
<li><p>Product list → React Query (server state, needs caching + refetching)</p>
</li>
<li><p>"Is the filter dropdown open?" → <code>useState</code> (local, nobody else cares)</p>
</li>
</ul>
<p>The common mistake is using <code>useState</code> for everything, then prop-drilling 5 levels deep. Or worse, putting <em>everything</em> in Redux, including whether a tooltip is visible. Global state is expensive to maintain — only put things there that genuinely need to be global.</p>
<p><strong>Step 4: The API layer — build a service layer</strong></p>
<p>Never, ever call <code>fetch</code> directly in a component. Build a service layer instead.</p>
<pre><code class="language-ts">// services/userService.ts
export const getUser = async (id: string) =&gt; {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error('Failed to fetch user');
  return res.json();
};
</code></pre>
<p>Then wrap it in a hook using React Query:</p>
<pre><code class="language-ts">// hooks/useUser.ts
import { useQuery } from '@tanstack/react-query';
import { getUser } from '../services/userService';

export const useUser = (id: string) =&gt; {
  return useQuery({
    queryKey: ['user', id],
    queryFn: () =&gt; getUser(id),
  });
};
</code></pre>
<p>And in your component:</p>
<pre><code class="language-tsx">// components/UserProfile.tsx
const { data: user, isLoading } = useUser('123');
</code></pre>
<p>The component has no idea how the data gets fetched. It just asks for it. This is the separation of concerns in action — and it makes testing dramatically easier.</p>
<p><strong>Step 5: Performance basics</strong></p>
<p>A few rules that apply to almost every React app:</p>
<p><strong>Lazy load your routes.</strong> Don't ship the entire app in one bundle.</p>
<pre><code class="language-tsx">const Dashboard = React.lazy(() =&gt; import('./pages/Dashboard'));
</code></pre>
<p><strong>Don't re-render what doesn't need to be re-rendered.</strong> <code>React.memo</code> on expensive components that receive the same props frequently:</p>
<pre><code class="language-tsx">const ExpensiveChart = React.memo(({ data }) =&gt; {
  // only re-renders if `data` changes
});
</code></pre>
<p><strong>Memoize expensive calculations</strong> with <code>useMemo</code>. Memoize stable function references with <code>useCallback</code> (especially when passing to child components or as <code>useEffect</code> deps).</p>
<p>These aren't premature optimizations if you're building something that's going to grow — they're just good habits.</p>
<p><strong>Step 6: Testing strategy (the short version)</strong></p>
<p>Three levels:</p>
<p>Three levels:</p>
<ul>
<li><p><strong>Unit tests</strong> → individual functions, hooks, pure logic</p>
</li>
<li><p><strong>Integration tests</strong> → user flows across multiple components (login form → redirects to dashboard)</p>
</li>
<li><p><strong>E2E tests</strong> → full user journeys in a browser (Cypress/Playwright)</p>
</li>
</ul>
<p>You don't need 100% coverage. But having <em>some</em> integration tests for your critical paths (checkout, auth, core feature) will save you from shipping broken stuff.</p>
<h3>Common mistakes I see (and have made)</h3>
<p>❌ <strong>API calls inside components</strong> — the #1 offender. Ties your UI directly to your data source.</p>
<p>❌ <strong>Everything in global state</strong> — modal open state does not need to be in Redux. I promise.</p>
<p>❌ <strong>Flat folder structure</strong> — works until it doesn't. Plan for scale from the start.</p>
<p>❌ <strong>Mixing UI and logic in the same component</strong> — a 300-line component that fetches data, transforms it, handles errors, and renders a table is doing too much.</p>
<h3>The mental model I keep coming back to</h3>
<p>Frontend architecture is just one question, asked repeatedly: <strong>"Which layer does this belong to?"</strong></p>
<p>If it renders → UI layer.</p>
<p>If it's a shared state → state layer.</p>
<p>If it talks to an API → data layer.</p>
<p>Everything else is details.</p>
<h3>What's next</h3>
<p>In the next post, I'm diving into how React's rendering actually works — what triggers a re-render, what doesn't, and how to read the React Profiler to find where your app is slow. That one's going to be very hands-on because I've been debugging a gnarly re-render issue at work for the past few weeks.</p>
<p>If you're learning frontend system design too, drop a comment — I would love to know what topics you're finding most confusing.</p>
]]></content:encoded></item></channel></rss>