← Back to principles

UI

Separate Core Logic from Framework Code

Statement

Keep business logic, state machines, and data transformations framework-agnostic. Framework-specific code should be a thin adapter layer. This makes logic testable, portable, and survivable across framework migrations.

What this means in practice

The valuable logic of the application — business rules, data transformations, validation, state machines, and domain models — lives in plain TypeScript modules with no framework imports. Framework code (React components, hooks, Angular services, etc.) acts as a thin adapter that connects this core logic to the UI.

  • Business rules and domain logic are expressed in pure functions and classes that can be imported and tested without rendering a component.

  • State machines and complex workflows are modelled independently of the framework's state management primitives.

  • Data transformations (mapping API responses, formatting for display, computing derived values) live in utility or service modules, not inside components.

  • Framework-specific code is limited to wiring: binding core logic to the rendering layer, handling lifecycle events, and managing UI-specific concerns like focus and animation.

  • When evaluating a new library or pattern, the question is: "Can the core logic survive if we swap the framework?" If the answer is no, the coupling is too tight.

Why this matters

Frameworks change; business logic endures.

  • The JavaScript ecosystem moves fast — frameworks evolve, fall out of favour, or introduce breaking paradigm shifts. Logic that is coupled to a specific framework must be rewritten when the framework changes.

  • Framework-agnostic logic is dramatically easier to unit test because it doesn't require component rendering, mocking framework internals, or setting up providers.

  • Separating concerns makes the codebase easier to reason about: developers can understand the domain logic without also understanding the framework's lifecycle, reactivity model, or rendering strategy.

  • Portable logic can be shared across different contexts — a web app, a CLI tool, a server-side process, or a different frontend framework — without duplication.

  • Teams that adopted this approach during the jQuery-to-React and AngularJS-to-Angular migrations retained their core logic while only rewriting the adapter layer.

  • This mirrors how TanStack builds every library: a framework-agnostic core with thin adapters for React, Vue, Solid, and others.

Practices that meet this principle

  • Place business logic, validation rules, and data transformations in standalone TypeScript modules with no framework imports.

  • Model complex state transitions using state machines or reducers in pure functions, then connect them to the framework's state management as a thin wrapper.

  • Keep components focused on rendering and user interaction — delegate computation, formatting, and decision-making to imported functions.

  • Write unit tests for core logic using plain test runners (Vitest, Jest) without needing component rendering utilities.

  • When creating a new feature, start by writing the core logic and its tests before building the UI layer around it.

  • Use dependency injection or function parameters to pass framework-provided values (routing, context, services) into core logic rather than importing framework APIs directly.

  • Audit existing components for embedded business logic and extract it incrementally into shared modules.

  • Treat the framework layer as replaceable: if you can imagine swapping React for another framework and only rewriting the components (not the logic), the separation is right.

Validation

A project meets this principle when:

  • Business rules and data transformations are unit-testable without rendering components or importing framework modules.

  • Core logic modules have no framework-specific imports (no react, @angular/core, vue, etc.).

  • Components are thin — they handle rendering, event binding, and lifecycle, but delegate logic to imported functions.

  • The same core logic could, in principle, be reused in a different framework or runtime without modification.

  • New features are developed with core logic and UI as separate, identifiable layers.

  • Code reviews flag business logic that is embedded in components and suggest extraction.

Potential blockers

  • Existing codebases with business logic deeply embedded in components, requiring incremental extraction.

  • Framework patterns that blur the line between logic and UI (e.g. heavily stateful hooks that mix data fetching, caching, and rendering concerns).

  • Team habits of writing "all in one component" code, requiring a cultural shift towards separation.

  • Perceived overhead of maintaining separate modules for small features where the benefit seems marginal.