August 21, 2025 · Monorepo

Designing Atomic Modules in a React Monorepo

Stop shipping one mega “shared” package for every surface area in your monorepo. Break functionality into atomic feature modules, wire them together with workspace protocols, and keep each team’s blast radius small.

Spotting the "Kitchen Sink" Package

In a typical monorepo you’ll see packages/my-ui pulling double duty for apps/web, apps/mobile, and apps/admin. Over time it grows god objects, cross-app imports, and circular dependencies. A quick audit usually reveals:

Restructure Around Features

Instead of one general-purpose package, model each feature or surface area as its own workspace. Keep things small, opinionated, and tagged by domain:

packages/
  @transactions/history-table
  @transactions/recurring-table
  @transactions/add-transfer-modal
  @video/player
  @video/editor
  @video/upload
  @ui/button
  @ui/modal

Guidelines that keep the architecture predictable:

Package Layout

Each workspace can be tiny, but a consistent skeleton makes onboarding painless:

packages/@accounts/add-modal/
  package.json
  src/
    components/
    hooks/
    services/api.ts      # network calls only
    services/utils.ts    # thin helpers wrapping api.ts
    constants/colors.ts
    index.ts

Packages that only expose a single function or hook can skip the folders—just guard against the "misc" dump by splitting things once they grow.

Managing Dependencies

Workspace protocols keep versions in sync without publishing every package. A typical package.json looks like:

{
  "name": "@accounts/add-modal",
  "version": "1.0.0",
  "main": "src/index.ts",
  "dependencies": {
    "@transactions/transfer-modal": "workspace:^",
    "@ui/form": "workspace:^",
    "@ui/modal": "workspace:^"
  },
  "peerDependencies": {
    "react": "^18",
    "react-native": "*"
  },
  "module": "dist/index.js"
}

Use Yarn 4 workspaces (or pnpm/Bun equivalents) to hoist shared deps, and declare peers for runtime libraries so each app decides which version to ship.

Version Consistency

If you pin versions inside packages, add a spec synchronizer like syncpack to avoid drift:

// .syncpackrc
{
  "indent": "    ",
  "versionGroups": [
    {
      "label": "whole monorepo",
      "specifierTypes": ["exact", "range", "workspace-protocol"]
    }
  ]
}

This setup ignores * wildcards (handy for internal React Native packages) while enforcing consistency everywhere else.

Conclusion