<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Keshav Agarwal</title>
    <description>The latest articles on Forem by Keshav Agarwal (@keshav422).</description>
    <link>https://forem.com/keshav422</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3811574%2F8ca8a698-d1c6-4eb6-9680-598c0e130357.jpeg</url>
      <title>Forem: Keshav Agarwal</title>
      <link>https://forem.com/keshav422</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/keshav422"/>
    <language>en</language>
    <item>
      <title>How We Built a Code Quality Pipeline That Roasts Bad PRs Before Humans Have To</title>
      <dc:creator>Keshav Agarwal</dc:creator>
      <pubDate>Sun, 15 Mar 2026 04:58:33 +0000</pubDate>
      <link>https://forem.com/keshav422/how-we-built-a-code-quality-pipeline-that-roasts-bad-prs-before-humans-have-to-24oo</link>
      <guid>https://forem.com/keshav422/how-we-built-a-code-quality-pipeline-that-roasts-bad-prs-before-humans-have-to-24oo</guid>
      <description>&lt;h2&gt;
  
  
  The Slow Descent Into "It's Fine, Ship It"
&lt;/h2&gt;

&lt;p&gt;Nobody wakes up one day and decides to ruin a codebase. It's more of a group activity that happens gradually, like a potluck where everyone brings potato salad.&lt;/p&gt;

&lt;p&gt;When our team was small — two or three devs who reviewed every line — code quality maintained itself through osmosis. We all knew the patterns. We all agreed on formatting. Nobody wrote an 800-line component because someone would notice and gently question their life choices.&lt;/p&gt;

&lt;p&gt;Then the team grew. More features, more devs, more PRs flying in simultaneously. And the cracks started showing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Components quietly ballooning past 700 lines — because "I'll refactor it later" (narrator: they did not refactor it later)&lt;/li&gt;
&lt;li&gt;Formatting roulette: tabs here, spaces there, trailing commas in this file but not that one&lt;/li&gt;
&lt;li&gt;Duplicate i18n messages across different feature modules — the same English string defined in three different &lt;code&gt;messages.js&lt;/code&gt; files with three different IDs, leading to translation mismatches and wasted localization budget (more on this — it's a bigger deal than it sounds)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;console.log('HERE')&lt;/code&gt; statements left behind like breadcrumbs from a debugging session that ended three sprints ago&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;var&lt;/code&gt; declarations showing up in a modern codebase, haunting us like a ghost from JavaScript past&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No single dramatic incident. No production outage. Just a slow, steady erosion of the standards we thought were obvious. Death by a thousand "LGTM, merge it" reviews.&lt;/p&gt;

&lt;p&gt;Code review alone couldn't save us — reviewers are human, PRs are long, and "does this file exceed 500 lines?" isn't the kind of thing you catch at 11 PM after your third coffee. We needed robots. Specifically, mean robots that block your PR and make you feel bad about your formatting choices.&lt;/p&gt;




&lt;h2&gt;
  
  
  "But What About CodeRabbit / AI Review Tools?"
&lt;/h2&gt;

&lt;p&gt;Fair question. We use CodeRabbit for PR reviews and it's great at catching logic issues and suggesting improvements. But on the free tiers, it suggests fixes — it doesn't enforce them. A developer can read CodeRabbit’s “you should fix this formatting” comment, nod thoughtfully… and merge anyway.&lt;/p&gt;

&lt;p&gt;We needed something with teeth. Something that physically prevents a merge until the code meets the bar. So we built our own enforcement layer with GitHub Actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Why Not Biome / oxlint / Modern Linters?"
&lt;/h2&gt;

&lt;p&gt;Also a fair question. New-gen linters like Biome are blazing fast and genuinely exciting. But our build pipelines are still on older Node versions, and our entire toolchain — Babel plugins, Webpack config, ESLint plugins for Redux-Saga, styled-components accessibility — is deeply integrated with the ESLint ecosystem. Migrating to Biome would mean rebuilding a lot of that plugin infrastructure.&lt;/p&gt;

&lt;p&gt;ESLint isn't the shiny new thing, but it's battle-tested, extensible, and — critically — it works with our current stack without a rewrite. Sometimes the boring tool that ships today beats the exciting tool that requires a migration quarter.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture: The Quality Gauntlet
&lt;/h2&gt;

&lt;p&gt;We ended up with a layered quality pipeline. Each layer catches different things at different stages, so by the time code reaches &lt;code&gt;master&lt;/code&gt;, it's been interrogated more thoroughly than a suspect in a crime drama.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────────────────────┐
│                    QUALITY PIPELINE                          │
│                 (a.k.a. "The Gauntlet")                      │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  The Foundation: THE RULEBOOK                                │
│  ┌────────────────────────────────────────────────┐          │
│  │  .eslintrc.yml — the opinionated ruleset       │          │
│  │  max-lines, complexity, prop-types, camelcase  │          │
│  │  + smart overrides for reducers/sagas/tests    │          │
│  └────────────────────────────────────────────────┘          │
│         │ powers both layers below                           │
│         v                                                    │
│  Layer 1: COMMIT TIME (local)                                │
│  ┌────────────────────────────────────────────────┐          │
│  │  lint-staged                                   │          │
│  │  • ESLint --fix on staged *.js files           │          │
│  │  • Prettier on staged *.json files             │          │
│  │  Catches: formatting, auto-fixable errors      │          │
│  └────────────────────────────────────────────────┘          │
│         │                                                    │
│         v                                                    │
│  Layer 2: PR TIME (GitHub Actions — run in parallel)         │
│  ┌──────────────┐ ┌─────────────────┐ ┌───────────┐          │
│  │ JS Linter    │ │ Duplicate i18n  │ │ Gitleaks  │          │
│  │ (full ESLint)│ │ (cross-file)    │ │ (secrets) │          │
│  └──────┬───────┘ └───────┬─────────┘ └─────┬─────┘          │
│         └────────────┬────┘                  │               │
│                      v                       │               │
│              Merge blocked until ALL pass ◄───┘              │
│                                                              │
└──────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Each layer exists because the previous one isn't enough.&lt;/strong&gt; The ESLint rulebook defines &lt;em&gt;what&lt;/em&gt; matters — it's the foundation that powers everything else. lint-staged catches formatting but only runs on staged files. GitHub Actions catches everything but only on PRs. All three together form the net. One layer alone is a suggestion. Three layers together are the law.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The PR checks run in parallel&lt;/strong&gt; — the linter, duplicate i18n check, and Gitleaks scan are independent workflows that kick off simultaneously. Since they don't share state, GitHub runs them on separate runners at the same time. For most repos, the entire suite finishes in under a minute. You push, you wait a few seconds, and you know exactly where you stand.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 1: Pre-Commit Hooks (lint-staged)
&lt;/h2&gt;

&lt;p&gt;The first line of defense runs before code ever leaves your machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lint-staged"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"*.js"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run lint:eslint:fix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"*.json"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prettier --write"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pre-commit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lint:staged"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every &lt;code&gt;git commit&lt;/code&gt; triggers this. JavaScript files get ESLint with &lt;code&gt;--fix&lt;/code&gt; — formatting issues, missing semicolons, trailing spaces are auto-corrected before you even see them. JSON files get Prettier. The developer doesn't have to remember anything. The formatting wars are over before they start.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you're on an older version of lint-staged (&amp;lt; v10), you'll need to add &lt;code&gt;"git add --force"&lt;/code&gt; after each command. v10+ handles re-staging automatically.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But lint-staged only touches staged files. If &lt;code&gt;ComponentA.js&lt;/code&gt; is clean but &lt;code&gt;ComponentB.js&lt;/code&gt; already has a linting error, lint-staged won't catch it. Also, a developer can bypass this entirely with &lt;code&gt;git commit --no-verify&lt;/code&gt;. (We trust our team, but we also trust automation more.)&lt;/p&gt;

&lt;p&gt;That's where the real enforcers come in.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 2: GitHub Actions — The PR Gatekeepers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Linter Workflow
&lt;/h3&gt;

&lt;p&gt;Every PR triggers a full ESLint pass across the entire codebase. Here's the workflow (condensed — the real one installs ~30 packages):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;JavaScript Linting Validator&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;reopened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;js-lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lts/*'&lt;/span&gt;

      &lt;span class="c1"&gt;# Nuclear option: clean slate every time&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rm -f .npmrc&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rm -rf node_modules package-lock.json&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm cache clean --force&lt;/span&gt;

      &lt;span class="c1"&gt;# Install only linting deps (not the whole project)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install ESLint + Prettier + Babel deps&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;npm install "@babel/core@^7.26.7" \&lt;/span&gt;
            &lt;span class="s"&gt;"@babel/eslint-parser@^7.26.5" \&lt;/span&gt;
            &lt;span class="s"&gt;"eslint@^8.57.0" \&lt;/span&gt;
            &lt;span class="s"&gt;"eslint-config-prettier@^10.0.1" \&lt;/span&gt;
            &lt;span class="s"&gt;"eslint-plugin-react@^7.37.4" \&lt;/span&gt;
            &lt;span class="s"&gt;"eslint-plugin-prettier@^5.2.3" \&lt;/span&gt;
            &lt;span class="s"&gt;"prettier@^3.4.2" \&lt;/span&gt;
            &lt;span class="s"&gt;# ... ~20 more packages&lt;/span&gt;
            &lt;span class="s"&gt;--no-save --silent&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx eslint .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth calling out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The clean-slate install.&lt;/strong&gt; We nuke &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;package-lock.json&lt;/code&gt;, &lt;code&gt;.npmrc&lt;/code&gt;, and the npm cache before installing. Aggressive? Yes. It ensures we're never tripped up by stale local artifacts. One tradeoff worth noting: deleting the lockfile means CI resolves versions independently from your local setup, so there's a slim chance of version drift between what devs run locally and what CI installs. For &lt;em&gt;linting-only&lt;/em&gt; deps this is a low risk we accept — but for production installs, you'd want to keep the lockfile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every PR, every branch.&lt;/strong&gt; Not just &lt;code&gt;master&lt;/code&gt; — &lt;em&gt;every&lt;/em&gt; branch. If your feature branch has a lint error, you know immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It blocks the merge.&lt;/strong&gt; This is a required check. No "I'll fix it in the next PR" escape hatch. The robot says no, the robot means no.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Duplicate i18n Check (Our Favorite)
&lt;/h3&gt;

&lt;p&gt;This one solves a problem that quietly costs real money at scale, and we haven't seen many teams talk about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; In a large internationalized codebase, many features define their own &lt;code&gt;messages.js&lt;/code&gt; file for &lt;code&gt;react-intl&lt;/code&gt;. Over time, different developers writing similar features independently define the same English string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In CreateEntity/messages.js&lt;/span&gt;
&lt;span class="nf"&gt;defineMessages&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;saveButton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app.createEntity.save&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;defaultMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Save&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// In EditEntity/messages.js (different dev, different sprint)&lt;/span&gt;
&lt;span class="nf"&gt;defineMessages&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;saveAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app.editEntity.save&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;defaultMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Save&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two different IDs, same &lt;code&gt;defaultMessage&lt;/code&gt;. In English, no one notices. But when you send these to a translation vendor, you're &lt;strong&gt;paying to translate the same string multiple times&lt;/strong&gt;. Worse — different translators might translate "Save" differently, so users see inconsistent text across the product. One screen says the equivalent of "Store" and another says "Keep." Subtle, but it erodes trust.&lt;/p&gt;

&lt;p&gt;At scale — dozens of languages, hundreds of message strings — these duplicates add up fast in translation costs and QA time.&lt;/p&gt;

&lt;p&gt;The workflow is dead simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check Duplicate i18n Messages&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;reopened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;check-duplicates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lts/*'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node scripts/check-duplicate-messages.js&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The custom script behind it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Recursively finds every &lt;code&gt;messages.js&lt;/code&gt; and &lt;code&gt;message.js&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;Regex-extracts all &lt;code&gt;{ id: '...', defaultMessage: '...' }&lt;/code&gt; definitions — handling single-line, multi-line, and template literal formats&lt;/li&gt;
&lt;li&gt;Builds a map of message values to their file locations&lt;/li&gt;
&lt;li&gt;Flags any &lt;code&gt;defaultMessage&lt;/code&gt; that appears in two or more different files&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When it fails, the output is clear:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Found 47 message files to check

CROSS-FILE DUPLICATE MESSAGE VALUE FOUND
Value: "Save"
  In app/components/pages/CreateEntity/messages.js:
    - ID: app.createEntity.save
  In app/components/pages/EditEntity/messages.js:
    - ID: app.editEntity.save

Cross-file duplicate message values found.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PR blocked. Fix is always the same: extract the shared string to a common messages file or reuse the existing definition. This single check has saved us from dozens of translation inconsistencies — and a non-trivial amount of translation spend.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gitleaks Secret Scan
&lt;/h3&gt;

&lt;p&gt;We also run Gitleaks on every PR to &lt;code&gt;master&lt;/code&gt;. It scans for accidentally committed secrets — API keys, tokens, credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gitleaks Secret Scan&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;reopened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;master&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gitleaks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gitleaks/gitleaks-action@v2&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Standard security hygiene. If you're not doing this, start today. It catches the kind of mistake that turns a normal Tuesday into an incident response.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Rulebook: ESLint Rules That Actually Matter
&lt;/h2&gt;

&lt;p&gt;The rules are the backbone. Anyone can set up a linter — the interesting part is &lt;em&gt;what you choose to enforce&lt;/em&gt;. Here's our opinionated ruleset with the "why" behind each choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;max-lines: 500&lt;/code&gt; — The God Component Killer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;max-lines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;error&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;500 lines is generous enough for complex forms but strict enough to prevent the 800-line monsters that nobody wants to review and everybody pretends they'll "refactor someday." If your component hits 500 lines, it's doing too much. Split it. Your future self (and your reviewers) will thank you.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;complexity: 10&lt;/code&gt; — Keep Functions Readable
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;complexity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;error&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cyclomatic complexity of 10 means a function can have at most 10 independent paths. This catches those deeply nested &lt;code&gt;if/else/switch&lt;/code&gt; pyramids that look like they were written during a fever dream. If your function has complexity 15, it needs decomposing.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;react/prop-types: error&lt;/code&gt; — Component Contracts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;react/prop-types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;error&lt;/span&gt;
&lt;span class="na"&gt;react/require-default-props&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, we know TypeScript exists. We're not using it (yet). PropTypes are the closest thing to a component contract in JS-land. When a new dev opens a component, &lt;code&gt;propTypes&lt;/code&gt; tells them exactly what it expects — no reading the entire render method to figure out what props exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;camelcase: error&lt;/code&gt; — One Convention to Rule Them All
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;camelcase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;error&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try reading a codebase where half the variables are &lt;code&gt;user_name&lt;/code&gt; and the other half are &lt;code&gt;userName&lt;/code&gt;. It's like reading a book where the author randomly switches between British and American spelling. Technically functional, spiritually painful.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;prettier/prettier: error&lt;/code&gt; — Formatting Is Not a Discussion
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;prettier/prettier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;error&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;singleQuote&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;printWidth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;100&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;endOfLine&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lf'&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prettier as an ESLint &lt;em&gt;error&lt;/em&gt; means formatting violations block the merge. Not warnings. Not suggestions. The specific settings (single quotes, 100-char lines) matter less than the fact that they're enforced uniformly. Tabs vs spaces debates belong in 2015 where we left them.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Smart Part: Overrides
&lt;/h3&gt;

&lt;p&gt;Not every file should follow the same rules. This is where the config stops being dogmatic and starts being practical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;overrides&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/reducer.js'&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;complexity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;off&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/saga.js'&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;complexity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;off&lt;/span&gt;
      &lt;span class="na"&gt;camelcase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;off&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*.test.js'&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;react/prop-types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;off&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/tests/**/*.test.js'&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;complexity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;off&lt;/span&gt;
      &lt;span class="na"&gt;camelcase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;off&lt;/span&gt;
      &lt;span class="na"&gt;max-lines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;off&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Reducers get unlimited complexity&lt;/strong&gt; because Redux reducers are fundamentally big switch statements. A reducer handling 15 actions has complexity 15 — that's not bad code, that's just how reducers work. Forcing artificial splits would make the code &lt;em&gt;worse&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sagas get complexity + camelcase exemptions&lt;/strong&gt; because sagas are the boundary layer where external API naming (&lt;code&gt;user_name&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt;) meets internal conventions. Forcing camelcase there means either renaming every API field or suppressing the rule on every line. Both are worse than the override.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tests get relaxed rules&lt;/strong&gt; because we want developers to write &lt;em&gt;more&lt;/em&gt; tests, not fewer. If prop-types or max-lines are blocking test coverage, the rules are hurting more than helping.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We'd Do Differently
&lt;/h2&gt;

&lt;p&gt;Honesty hour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Outdated Node in CI.&lt;/strong&gt; Our linter workflow was pinned to an older Node version for a while — the kind of tech debt that works fine until it doesn't, like driving with an expired inspection sticker. Upgrading means verifying 30+ Babel and ESLint packages resolve correctly on the newer runtime. We got there eventually, but it sat on the "we'll get to it" list longer than we'd like to admit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The clean-slate install is slow.&lt;/strong&gt; Nuking &lt;code&gt;node_modules&lt;/code&gt; and reinstalling from scratch on every PR isn't fast. Dependency caching (&lt;code&gt;actions/cache&lt;/code&gt;) would fix this. We traded speed for reproducibility, but you can have both.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;If you're setting up quality gates for a multi-developer codebase, here's the framework:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with the rules.&lt;/strong&gt; Define what "good code" means in your linter config. Be opinionated — it's easier to relax rules than to introduce them into a codebase that's already feral.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enforce locally.&lt;/strong&gt; lint-staged + pre-commit hooks fix the easy stuff automatically. Zero friction.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enforce in CI.&lt;/strong&gt; GitHub Actions catch what local hooks miss — and unlike pre-commit hooks, they can't be skipped with &lt;code&gt;--no-verify&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Write overrides, not exceptions.&lt;/strong&gt; Instead of &lt;code&gt;// eslint-disable-next-line&lt;/code&gt; scattered across hundreds of files, configure overrides by file pattern. Encode your team's decisions in the config, not in comments.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Build custom checks for your pain.&lt;/strong&gt; The duplicate i18n check isn't in any plugin — we wrote it because it was our problem. Every codebase has unique failure modes. Write scripts to catch yours.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal isn't zero lint warnings. The goal is that a new developer can join the team, open a PR, and the pipeline tells them exactly how the team writes code — without anyone writing a Confluence doc that nobody reads.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Running custom quality checks in your CI that go beyond standard linting? I'd love to hear what problems they solve. Drop them in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>devops</category>
      <category>eslint</category>
      <category>react</category>
    </item>
    <item>
      <title>How We Managed State Across 900+ React Components Without Losing Our Minds</title>
      <dc:creator>Keshav Agarwal</dc:creator>
      <pubDate>Sat, 07 Mar 2026 16:11:24 +0000</pubDate>
      <link>https://forem.com/keshav422/how-we-managed-state-across-900-react-components-without-losing-our-minds-56mj</link>
      <guid>https://forem.com/keshav422/how-we-managed-state-across-900-react-components-without-losing-our-minds-56mj</guid>
      <description>&lt;h2&gt;
  
  
  The Bug That Changed Everything
&lt;/h2&gt;

&lt;p&gt;A few months into building our enterprise SaaS platform, a customer reported a bizarre issue: saving a reward configuration would silently overwrite pricing data that another team member had edited seconds earlier.&lt;/p&gt;

&lt;p&gt;The root cause? Two components — three levels apart in the tree — were each managing their own copy of the same entity. One saved stale data over fresh data. No error. No warning. Just silent data loss.&lt;/p&gt;

&lt;p&gt;That incident killed any remaining appetite for scattered local state. This happened in a codebase with &lt;strong&gt;900+ components&lt;/strong&gt; across &lt;strong&gt;18+ pages&lt;/strong&gt;, multiple devs contributing simultaneously, multi-step creation flows with cross-step validation, and millions of end users.&lt;/p&gt;

&lt;p&gt;We went all-in on &lt;strong&gt;centralized state management&lt;/strong&gt; with Redux — and it became the single best architectural decision we made. (Also the most verbose one, but we'll get to that.)&lt;/p&gt;

&lt;p&gt;Redux wasn't just our data store. It was the &lt;strong&gt;coordination layer&lt;/strong&gt; — the air traffic control for components that otherwise had no business knowing about each other. A setting change in Component A causing Component B to refresh, a sidebar selection updating a detail panel three levels away — all orchestrated through Redux actions and sagas instead of prop callbacks or tangled &lt;code&gt;useEffect&lt;/code&gt; chains.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture: Feature-Based Redux Modules
&lt;/h2&gt;

&lt;p&gt;Every feature owns a self-contained Redux module with five files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/features/CreateEditEntity/
  constants.js    # Namespaced action types
  actions.js      # Action creator functions
  reducer.js      # State transitions (Immutable.js)
  saga.js         # Async side effects (Redux-Saga)
  selector.js     # Memoized state selectors (Reselect)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, five files per feature. I know, I know — I can hear you groaning. I'll address the boilerplate elephant later.&lt;/p&gt;

&lt;p&gt;But this structure meant &lt;strong&gt;any developer could find the business logic for any feature in seconds&lt;/strong&gt;. No "where does this state live?" Slack messages at 3 PM. Each slice owned a single concern: &lt;code&gt;createEditEntity&lt;/code&gt; handled only the reward creation flow, &lt;code&gt;configSettings&lt;/code&gt; handled only category config. No god-reducers trying to manage the entire known universe.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Stack
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;react&lt;/strong&gt; ^18.2.0&lt;/td&gt;
&lt;td&gt;UI library&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;redux&lt;/strong&gt; 4.0.1&lt;/td&gt;
&lt;td&gt;Core state container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;react-redux&lt;/strong&gt; 5.1.0&lt;/td&gt;
&lt;td&gt;React bindings (&lt;code&gt;connect&lt;/code&gt;, &lt;code&gt;Provider&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;redux-saga&lt;/strong&gt; 0.16.2&lt;/td&gt;
&lt;td&gt;Generator-based side effects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;immutable&lt;/strong&gt; 4.3.7&lt;/td&gt;
&lt;td&gt;Persistent immutable data structures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;reselect&lt;/strong&gt; 4.0.0&lt;/td&gt;
&lt;td&gt;Memoized selectors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;webpack&lt;/strong&gt; ^5.91.0&lt;/td&gt;
&lt;td&gt;Bundler with Module Federation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Not the latest versions — and that's part of the story. This architecture was established years ago and has stayed stable through multiple product evolutions. If it ain't broke, don't npm install.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic Injection: Load Only What You Need
&lt;/h3&gt;

&lt;p&gt;Not every feature loads on the first page. Reducers and sagas are injected into the store only when their component mounts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;withReducer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;injectReducer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;createEditEntity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reducer&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;withSaga&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;injectSaga&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;createEditEntity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;saga&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;withReducer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;withSaga&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;withConnect&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nx"&gt;CreateEditEntity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A user who only visits a listing page never downloads the reducer logic for the creation flow. Webpack decides &lt;em&gt;when&lt;/em&gt; code loads - Redux decides &lt;em&gt;how&lt;/em&gt; it integrates into the store.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Three Patterns That Actually Mattered
&lt;/h2&gt;

&lt;p&gt;Out of everything we built, three patterns delivered outsized value. If you take nothing else from this post, take these. (And maybe a coffee. This is the meaty part.)&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Sagas as the Business Logic Layer (The Biggest Win)
&lt;/h3&gt;

&lt;p&gt;This is the hill I'll die on. &lt;strong&gt;Sagas absorbed the business logic that would otherwise pollute components.&lt;/strong&gt; Components became pure render functions — they dispatch intents and display state. Everything messy? That's the saga's problem now.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Navigation&lt;/strong&gt; — after a successful save, the saga dispatches a route change - the component never calls &lt;code&gt;history.push&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notifications&lt;/strong&gt; — success/error toasts are triggered by sagas, not component-level &lt;code&gt;useEffect&lt;/code&gt; chains&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-feature coordination&lt;/strong&gt; — when a reward is saved, a saga dispatches an action that marks the rewards list cache as stale, so the next visit triggers a refresh&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependent actions&lt;/strong&gt; — after saving entity A, dispatch actions to refresh entity B's list, invalidate a cache slice, and update a sidebar count — without the originating component knowing about these dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To make this concrete, here's our &lt;strong&gt;actual reward creation flow&lt;/strong&gt; — a 5-step wizard where the saga orchestrates everything end-to-end. No code wall this time, just the flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│                    EDIT / DUPLICATE FLOW                    │
│                  (initializeGetByIDSaga)                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  User navigates to /edit/:entityId                          │
│         │                                                   │
│         v                                                   │
│  1. FETCH ─── call(Api.getEntityById, entityId)             │
│         │                                                   │
│         v                                                   │
│  2. EXTRACT PRICING MODE from nested configs                │
│    (buried 3 levels deep in the API response, obviously)    │
│         │                                                   │
│         v                                                   │
│  3. PARALLEL FETCH T&amp;amp;C ─── filter languages with T&amp;amp;C URLs   │
│    │         │         │    then fetch ALL in parallel      │
│    v         v         v    (no waterfall nonsense)         │
│   [en]     [fr]      [de]  ─── convert to text              │
│    │         │         │                                    │
│    └────┬────┘─────────┘                                    │
│         v                                                   │
│  4. DISPATCH per-language T&amp;amp;C updates to Redux              │
│         │                                                   │
│         v                                                   │
│  5. DISPATCH GET_ENTITY_BY_ID_SUCCESS                       │
│    └─&amp;gt; reducer calls hydrateFormState() which transforms    │
│        the flat API blob into our 5-step form state:        │
│        name, description, audience, payment, inventory,     │
│        languages, categories, restrictions...               │
│                                                             │
│  ✓ Form is now pre-filled. User edits away.                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That handles loading and populating the form. Now here's what happens when the user clicks Save — and this is where it gets interesting, because the saga has to gather state from 6 different Redux slices and assemble a single API payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│                       SAVE FLOW                             │
│                    (saveEntitySaga)                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  User clicks "Save" ─── dispatches SAVE_ENTITY_REQUEST      │
│         │                                                   │
│         v                                                   │
│  1. GATHER STATE from 6 Redux slices:                       │
│    ┌────────────────────────────────────────────┐           │
│    │  orgData ─── tenantData ─── configData.    │           │
│    │  formState ─── pricingMode ─── customFields│           │ 
│    └────────────────────────────────────────────┘           │
│         │                                                   │
│         v                                                   │
│  2. TRANSFORM via generateSavePayload()                     │
│    UI state ──────────────────────────&amp;gt; API payload         │
│    ┌──────────────────────────────────────────┐             │
│    │  Step 0: Basic meta (dates → UTC)        │             │
│    │  Step 1: Targeting (segments, tiers)     │             │
│    │  Step 2: Pricing &amp;amp; billing configs       │             │
│    │  Step 3: Limits (per-user, global)       │             │
│    │  Step 4: Localization (multi-lang)       │             │
│    │  Step 5: Additional (tags, groups, cats) │             │
│    └──────────────────────────────────────────┘             │
│         │                                                   │
│         v                                                   │
│  3. DETECT MODE ─── CREATE or EDIT?                         │
│         │                                                   │
│         v                                                   │
│  4. API CALL ─── Api.createEntity or Api.editEntity         │
│         │                                                   │
│         ├──── success ──&amp;gt; dispatch SUCCESS                  │
│         │                 push analytics event              │
│         │                 navigate to /listing              │
│         │                 show toast ("Go grab a coffee")   │
│         │                                                   │
│         └──── failure ──&amp;gt; dispatch FAILURE                  │
│                           show error toast                  │
│                           (we blame the API, naturally)     │
│                                                             │
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The beauty of this? The &lt;strong&gt;component&lt;/strong&gt; does almost nothing interesting.&lt;/p&gt;

&lt;p&gt;It renders form fields across 5 steps, dispatches &lt;code&gt;SET_*&lt;/code&gt; actions on keystrokes (&lt;code&gt;SET_NAME&lt;/code&gt;, &lt;code&gt;SET_DESCRIPTION&lt;/code&gt;, &lt;code&gt;SET_TARGET_SEGMENT&lt;/code&gt;, &lt;code&gt;SET_LIMITS&lt;/code&gt;...), and dispatches &lt;code&gt;SAVE_ENTITY_REQUEST&lt;/code&gt; when the user clicks Save. That's it.&lt;/p&gt;

&lt;p&gt;The component doesn't know how to transform data, doesn't know which API to call, doesn't know about T&amp;amp;C URL fetching, doesn't care about analytics. All that orchestration — loading, transforming, assembling across 6 reducers, saving, tracking, navigating — lives in the sagas. The component just renders and dispatches. No &lt;code&gt;useEffect&lt;/code&gt; chains. No "which lifecycle method does this run in?" existential crises.&lt;/p&gt;

&lt;p&gt;Any time you find yourself writing &lt;code&gt;useEffect(() =&amp;gt; { if (saveSuccess) { navigate(...); showToast(...); trackEvent(...); } })&lt;/code&gt;, that logic belongs in a saga. The saga owns the &lt;em&gt;entire chain of consequences&lt;/em&gt; from a single user intent.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Concurrent Uploads with eventChannel + fork
&lt;/h3&gt;

&lt;p&gt;This pattern handled &lt;strong&gt;hundreds of concurrent media uploads in production&lt;/strong&gt;. We needed bulk uploads of images, videos, and rich content to S3 — each tracked independently with progress, error state, and retry capability.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createUploadChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;eventChannel&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;emitter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;xhr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;XMLHttpRequest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;xhr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onprogress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;emitter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loaded&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;xhr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;emitter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="nf"&gt;emitter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;END&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;xhr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;emitter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="nf"&gt;emitter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;END&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;xhr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/upload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;xhr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;xhr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// cleanup = cancellation&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;uploadSingleFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;createUploadChannel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UPDATE_PROGRESS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FILE_UPLOAD_SUCCESS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;uploadAllFiles&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uploadSingleFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Wait for all uploads OR a cancel action — whichever comes first&lt;/span&gt;
  &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;race&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toPromise&lt;/span&gt;&lt;span class="p"&gt;())),&lt;/span&gt;
    &lt;span class="na"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CANCEL_ALL_UPLOADS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;eventChannel&lt;/code&gt; bridges callback-based APIs into the saga world. &lt;code&gt;fork&lt;/code&gt; spawns concurrent tasks. &lt;code&gt;race&lt;/code&gt; lets the user cancel everything mid-flight. Try building this with &lt;code&gt;useState&lt;/code&gt; and &lt;code&gt;useEffect&lt;/code&gt; — I'll wait. (Actually, don't. You'll end up with a ref-heavy, leak-prone mess and a therapy bill.)&lt;/p&gt;

&lt;h3&gt;
  
  
  3. No Prop Drilling — Components Connect Directly
&lt;/h3&gt;

&lt;p&gt;This is what makes Redux scale in component-heavy apps. In our codebase, components at every level connect directly to the store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parent page&lt;/strong&gt; connects to 4 selectors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Child form&lt;/strong&gt; independently connects to 9&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sibling accordion&lt;/strong&gt; connects to 13&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deeply nested molecule&lt;/strong&gt; (3 levels deep) connects to 5 selectors spanning multiple reducer domains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;None of these pass data to each other through props.&lt;/strong&gt; Each component declares exactly which state slices it needs. Adding a new data dependency to the nested molecule requires zero changes to any parent component.&lt;/p&gt;

&lt;p&gt;We used &lt;code&gt;createStructuredSelector&lt;/code&gt; + factory selectors (&lt;code&gt;makeSelect*&lt;/code&gt;) to ensure each connected component gets its own memoization cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mapStateToProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createStructuredSelector&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;entityDetails&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;makeSelectEntityDetails&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;makeSelectIsLoading&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;withConnect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mapStateToProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mapDispatchToProps&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;withReducer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;withSaga&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;withConnect&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nx"&gt;EntityComponent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What Bit Us (And What We'd Do Differently)
&lt;/h2&gt;

&lt;p&gt;Look, no architecture survives contact with reality unscathed. Here's where ours drew blood — and how we'd fix each one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Immutable.js Was a Mistake → Use Immer
&lt;/h3&gt;

&lt;p&gt;I'll say it plainly. Every boundary in our code has a tax:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reading a value? &lt;code&gt;state.get('name')&lt;/code&gt; instead of &lt;code&gt;state.name&lt;/code&gt; — because apparently dot notation was too easy&lt;/li&gt;
&lt;li&gt;Passing to a child component? &lt;code&gt;.toJS()&lt;/code&gt; — which creates a &lt;strong&gt;new object reference every render&lt;/strong&gt;, defeating the very memoization you set up five minutes ago&lt;/li&gt;
&lt;li&gt;Receiving from an API? &lt;code&gt;fromJS(response)&lt;/code&gt; — easy to forget, leading to subtle bugs where a component gets an Immutable Map instead of a plain object and React just... renders &lt;code&gt;[object Object]&lt;/code&gt; with a straight face&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;New developers would spend their first week debugging issues that boiled down to "you forgot &lt;code&gt;.toJS()&lt;/code&gt;" or "you called &lt;code&gt;.get()&lt;/code&gt; on a plain object." The cognitive overhead never went away. It's like a toll booth on every data highway in the app.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; RTK uses Immer under the hood. Write &lt;code&gt;state.name = 'foo'&lt;/code&gt; and get immutable updates for free. No more &lt;code&gt;.get()&lt;/code&gt; / &lt;code&gt;.set()&lt;/code&gt; / &lt;code&gt;.toJS()&lt;/code&gt; gymnastics. This is the change we want most.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Five Files Per Feature Is a Lot → RTK createSlice
&lt;/h3&gt;

&lt;p&gt;Every new feature: constants, actions, reducer, saga, selector. For a simple "show advanced options" toggle, a junior developer has to touch all five files plus wire up &lt;code&gt;injectReducer&lt;/code&gt;. That's the kind of ceremony that makes people question their career choices.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; &lt;code&gt;createSlice&lt;/code&gt; collapses constants + actions + reducer into one file. One file per feature instead of five — that alone would cut our boilerplate by 60%.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And here's the twist: AI-driven development has largely neutralized this pain in the meantime. Tools like Cursor and Claude can generate a complete feature module from a single prompt. The repetitive, predictable patterns that make Redux verbose are precisely what make AI assistance highly effective. What used to be 30 minutes of boilerplate is now 2 minutes of AI-generated code plus a quick review.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sagas Are Hard to Debug → Keep for Complex Flows, RTK Query for the Rest
&lt;/h3&gt;

&lt;p&gt;Stack traces through generator functions are opaque. The declarative effect model (&lt;code&gt;call&lt;/code&gt;, &lt;code&gt;put&lt;/code&gt;, &lt;code&gt;select&lt;/code&gt;) means you can't just set a breakpoint and step through — you have to understand the saga middleware's execution model. We had a saga that silently swallowed errors for &lt;strong&gt;weeks&lt;/strong&gt; because a &lt;code&gt;try/catch&lt;/code&gt; was in the wrong place. Finding it required manually stepping through the generator yields like an archaeologist brushing dirt off pottery.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; RTK Query for straightforward data fetching — it handles loading states, caching, and cache invalidation out of the box. Keep sagas only for complex orchestration flows where they genuinely shine (like the reward creation wizard above).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  connect() HOCs Feel Dated → Hooks
&lt;/h3&gt;

&lt;p&gt;Our codebase uses &lt;code&gt;connect()&lt;/code&gt; rather than &lt;code&gt;useSelector&lt;/code&gt;/&lt;code&gt;useDispatch&lt;/code&gt; hooks. This leads to deeply nested component trees in React DevTools — it's HOCs all the way down. Functional, battle-tested — but it has that "we built this before hooks existed" energy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; &lt;code&gt;useSelector&lt;/code&gt; and &lt;code&gt;useDispatch&lt;/code&gt; are simpler, more composable, and produce cleaner component code. No more HOC wrapper hell in React DevTools.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The silver lining: AI-assisted migration
&lt;/h3&gt;

&lt;p&gt;We're not stuck with these pain points forever. With tools like Claude and Cursor, migrating away from Immutable.js, converting HOCs to hooks, and collapsing five-file modules into RTK slices isn't a quarter-long rewrite — it's a series of well-prompted afternoons. The same repetitive patterns that make Redux verbose make it a perfect target for AI-assisted refactoring. We're chipping away at the tech debt one feature at a time, and it's going faster than anyone expected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The core architecture stays.&lt;/strong&gt; Feature-based modules with a centralized store remain the right choice at this scale. The organizational pattern scales - only the implementation details change.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;Redux gets a bad reputation for boilerplate, and honestly? Some of that is deserved. Writing five files to add a boolean toggle does feel like filling out government paperwork.&lt;/p&gt;

&lt;p&gt;But for a complex, multi-team enterprise application where state consistency and debuggability matter more than velocity on small features, centralized Redux gave us something invaluable: &lt;strong&gt;predictability at scale&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;900+ components. 18+ pages. Millions of users. Multiple devs. Quarters of active development. Zero "where does this state live?" mysteries. That's a trade-off we'd make again.&lt;/p&gt;

&lt;p&gt;Pick your tools based on your complexity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For local UI state, &lt;code&gt;useState&lt;/code&gt; is perfect — don't let anyone shame you into Redux for a modal toggle.&lt;/li&gt;
&lt;li&gt;For server state caching, RTK Query or TanStack Query are excellent.&lt;/li&gt;
&lt;li&gt;But when your app becomes a coordination problem — when components across different routes need to react to each other's changes — a centralized store with a proper orchestration layer is hard to beat.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Have you migrated a large Redux codebase to RTK, swapped Immutable.js for Immer, or converted connect() HOCs to hooks? What broke first? Drop your war stories below.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Shoutout to Anurag Volety for the early guidance and mentorship that shaped how I think about building UI at scale.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>redux</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
