← 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.