Developers often swap the words “component” and “module” as if they mean the same thing. They don’t, and treating them as synonyms quietly complicates architecture decisions.
Understanding the difference keeps codebases easier to extend, test, and hand to new teammates. The next sections unpack each term, show where they overlap, and give practical rules for choosing one over the other.
Core Definitions in Plain Language
A component is a small, replaceable piece that delivers a visible feature inside a larger system. It rarely lives alone and usually needs siblings to be useful.
Modules sit one level higher. They group related code behind a boundary and expose a minimal public surface to the rest of the application.
Think of a calendar widget on a web page as a component. The entire date-time library that powers it is the module.
Component Characteristics
Components are UI-centric in most modern stacks. They accept props, render output, and fire events upward.
They should be stateless when possible, pushing persistent state to a parent or a store. This keeps them cheap to recreate and safe to toss during hot-reload cycles.
Module Characteristics
Modules export functions, classes, or constants that other files can import. Their inner workings stay hidden unless explicitly exposed.
Good modules hide complexity so callers can solve problems without learning implementation trivia. They version themselves and can be published to package registries.
Granularity Gap
Granularity is the first practical gap you feel when organizing code. A button atom is a component, but bundling every button helper into one folder forms a module.
Teams that ignore this gap end up with either tiny scattered modules or giant 2 000-line component files. Both extremes slow onboarding and invite bugs.
Rule of thumb: if the unit is worth duplicating across projects, promote it to a module. If it only makes sense inside one screen, keep it a component.
Dependency Rules
Components may depend on modules, but modules should never depend on components. This directional rule prevents circular nightmares and keeps unit tests fast.
When a module imports a React card component, any CLI script that needs the module suddenly requires a DOM mock. That friction signals an architectural slip.
Keep shared business rules in modules, then inject them into components through props or context. The dependency arrow always points inward toward pure logic.
Packaging and Distribution
Modules ship easily. You publish a folder to npm, and every consumer gets semantic versioning and changelog notes for free.
Components are harder to version because they often rely on global styles or framework runtime quirks. Publishing a raw Vue SFC rarely works without peer dependencies and build steps.
Extract the framework-agnostic parts into a module first, then wrap thin framework-specific components around it. This two-layer approach maximizes reuse and minimizes breakage.
Testing Strategies
Unit tests for modules look like ordinary function tests: input in, assertion out. They run in Node without browsers, so they stay sub-second.
Component tests need mounting libraries and often spin up headless Chrome. They assert on DOM attributes, color contrast, and user events.
Splitting the heavy visual checks from the pure logic cuts CI time in half. Teams that keep both test types in the same file usually suffer flaky suites.
State Ownership Boundaries
State lives in one of three places: inside a module, inside a component, or in a global store. Choosing the wrong home creates spooky action at a distance.
Modules can own durable state if they expose clear getters and setters. A tiny locale module that remembers the chosen language is safer than prop-drilling it through ten component layers.
Ephemeral UI state—like whether a tooltip is open—belongs inside the component that renders it. Promoting that flag to a module adds noise and race conditions.
Reusability Patterns
Reusability is not an automatic virtue. A component reused in the wrong context carries hidden assumptions about styling, data shape, and event names.
Modules reuse more safely because they present contracts: typed signatures, JSDoc lines, and thrown errors. Callers know when they break the contract because the build fails.
Before copying a component to a second project, strip it down to a module that exports pure data and functions. Wrap project-specific components around that core separately.
Framework Specifics
React popularized the functional component, but its ecosystem also uses ES modules. A .jsx file can be both a component and a module at once, which confuses beginners.
Angular splits the ideas cleanly: @Component classes live beside @NgModule decorators. The compiler errors if you mix them up, so the learning curve is steeper but clearer.
Vue’s single-file components feel self-contained, yet they still import utility modules. Recognizing the seam helps you decide what to extract when the script section grows past a hundred lines.
React Example
A Button.jsx that imports classnames and forwards refs is a component. A format-label.js helper that Button calls is a module.
Keep the helper in a sibling file, test it in isolation, and share it with Link.jsx without dragging React into the test harness.
Angular Example
A SharedModule provides CommonModule and FormsModule to lazy-loaded features. A ButtonComponent declared inside SharedModule is still just a component, not a module in the architectural sense.
Promote pure utilities to a CoreModule that never declares components. This split prevents SharedModule from becoming a dumping ground.
Micro-frontend Considerations
Micro-frontends slice apps by business domain, not by component tree. Each mini-app owns its modules and registers components at runtime through a shell.
Shared component libraries tempt teams to enforce visual consistency, but versioning them across independently deployed apps is painful. Prefer shared modules that expose tokens, themes, and data contracts instead.
Let each micro-frontend wrap those tokens in local components that can evolve without a coordinated release train.
Monorepo Layout Tactics
In a monorepo, every folder under /packages is technically a module. Still, some packages export React components while others export math helpers.
Mark component packages with a prefix like @acme/ui-button to signal higher coupling and stricter peer dependency rules. Keep logic packages plain: @acme/date-utils.
This naming convention alone prevents a server-only service from accidentally pulling in a CSS-in-JS runtime.
Performance Impacts
Tree-shaking works best on modules with small, explicit exports. A 50 kB component file that re-exports everything defeats the optimizer.
Split heavy components into a thin shell and a lazy-loaded module. The shell renders a spinner while the module loads, improving time-to-interactive without touching the router config.
Measure bundle size at the module boundary, not the component level. A 5 kB module used by ten components is cheaper than ten 2 kB components that duplicate helpers.
Team Communication Tips
Name folders after the pattern they hold: /components for view tiles, /modules for stateful services. When a developer moves a file, the destination path answers “is this reusable?” without a README.
During code review, ask “does this pull in UI?” for every new import. If yes, the file belongs in components. If no, challenge why it isn’t a module.
Document the rule in a one-page decision record and pin it in Slack. Consistent vocabulary beats lengthy architecture docs that no one reads after week one.
Migration Checklist
Legacy codebases mix components and modules in the same folder. Start by identifying files that export JSX or HTML templates; tag them components.
Extract pure functions into a sibling modules folder, add unit tests, and update imports in small pull requests. Celebrate each merge to keep momentum.
After migration, enforce the boundary with an ESLint rule that bans modules from importing anything in the components tree. The build becomes the coach.