Skip to content

When the frontend fights its framework

Why imperative OOP thinking often works in the backend — but fights Angular, React, and Vue in modern frontend.

The backend processes transactions. The frontend keeps situations consistent.

The project used Angular.

At least, Angular was listed in package.json.

At first glance, everything was there: components, templates, services, routing, forms, HTTP calls. A modern frontend, in other words. Technically speaking.

Architecturally, it looked different.

Routing wasn’t really thought through via the Angular Router — it was supplemented or replaced by a homegrown routing system. Validation wasn’t modeled through forms, validators, or a clear view-state model — it ran through custom validation logic. Change detection wasn’t understood — it was worked around with custom UI-synchronization mechanisms. State management wasn’t thought of as state, event, and derivation — it was built through custom cache classes.

That sounds harsh. It’s not meant as an accusation.

Every single decision was probably explainable in isolation. The team wanted to solve problems. The team wanted control. The team wanted to ship. Nobody wakes up in the morning thinking, “Today I’m going to build a counter-framework inside Angular.”

That’s exactly why the pattern is dangerous.

It doesn’t come from stupidity. It comes from competence in a different context.

A lot of very good developers spent years training on object-oriented thinking, services, methods, transactions, and linear workflows. Through their degree. Through classic software engineering. Through backend projects. Through enterprise systems. Through frameworks where a request begins, gets processed, and ends.

That’s not wrong.

It just has a different home port.

A typical backend flow often looks roughly like this:

Request comes in. Validation runs. Data gets loaded. Rules get checked. Data gets saved. Response goes out.

That fits imperative thinking well.

The central question is:

What happens next?

It also fits classic OOP thinking well.

The central question is:

Which object is responsible? Which method do I call?

That makes sense in a lot of backend contexts. A use case orchestrates a workflow. A service encapsulates business logic. A repository loads data. A transaction bounds the operation. In the end, there’s a result.

The operation has a beginning. It has an end. In between, there’s a traceable sequence.

Frontend feels similar at first. A user clicks. A method runs. A service gets called. Data comes back. The UI updates.

So you carry over the model you already know.

The problem is: a UI is not a single request.

The backend often processes a transaction. The frontend keeps an ongoing situation consistent.

And that’s exactly where the thinking tips over.

Imperative means: the code describes which steps should run one after another.

For example:

async save(): Promise<void> {
this.loading = true;
this.error = null;
try {
const result = await this.api.save(this.formValue);
this.items = await this.api.loadItems();
this.toast.success('Saved');
this.router.navigate(['/items', result.id]);
} catch (error) {
this.error = 'Save failed';
this.toast.error('Save failed');
} finally {
this.loading = false;
}
}

That’s understandable.

It’s linear. It looks controllable. It works on the happy path. It’s easy to debug as long as not much happens at once.

Click → Save → Success → Toast → Navigate.

That feels familiar. Especially for developers coming from backend, OOP, or classic application development.

And to be clear: imperative code isn’t fundamentally bad. Of course there are imperative spots in the frontend. A button click is an event. A navigation is an effect. An HTTP request gets triggered. A dialog gets opened.

The problem doesn’t start wherever a single piece of imperative code sits.

The problem starts where the entire UI gets built as a manually steered sequence of steps.

Then every state turns into a flag. Every edge case turns into a sequence. Every UI reaction turns into a method call. Every inconsistency turns into another helper.

And eventually the screen no longer consists of a model — it consists of flow coordination.

OOP tends to think in objects, responsibilities, and methods.

An object encapsulates data and behavior. A service executes an action. A controller orchestrates a flow. A use case describes a transaction.

In backend systems, that can be very helpful. A lot of processes there are bounded. They run within a request, a message, a job, or a transaction. You can divide up responsibility, inject dependencies, and encapsulate behavior.

In frontend, though, this thinking quickly slides into an awkward spot.

The component turns into a little controller. It loads data. It holds cache state. It validates. It decides on visibility. It synchronizes the template and the model. It reacts to route changes. It manages loading states. It shows toasts. It navigates. It handles errors. It triggers reloads.

Eventually it’s no longer just a component.

It’s a controller, a process manager, a cache manager, a validator, and a render coordinator, all at once.

An Angular component is not a small backend controller with a template.

This isn’t just true for Angular. React components aren’t small controllers with JSX either. Vue components aren’t flow scripts with template syntax.

Modern frontend components are part of a reactive runtime model.

And you have to take that model seriously.

Reactive often sounds bigger than it needs to.

You don’t have to turn it into an academic debate on first principles. For everyday work, this idea is enough to start:

Reactive means not every step gets manually steered. Instead, you model states, events, and derivations.

Not:

Show the spinner now.

Instead:

The status is loading, so the UI shows the spinner.

Not:

Hide the error message, re-render the list, and then check the empty state.

Instead:

Whether the error, list, empty state, or loading state is visible follows from state and the ViewModel.

Not:

When that happens, set this flag, clear that array, trigger a reload, and then trigger the UI.

Instead:

An intent changes state. The next projection follows from that state.

Reactive doesn’t ask first:

What do I need to run right now?

Reactive asks:

What situation exists — and what follows from it?

Or, shorter:

Imperative asks: what happens next? Reactive asks: what follows from the state?

That’s a massive mental shift.

Not because reactive sounds “more modern.” But because a UI lives on continuously. It stays open while users interact, requests run, routes change, components disappear, data gets loaded in the background, and validation shifts while someone’s still typing.

A UI is less of a workflow.

It’s an ongoing projection.

UI isn’t a sequence of steps. UI is a projection.

Modern frontends need less flow control and more of a state model.

You can roughly break down the difference like this:

Mental modelCentral ideaTypical questionStrengthDanger in frontend
ImperativeFlow, sequence, method callsWhat happens next?Understandable step sequencesManual UI synchronization
OOPObject, method, responsibility, encapsulationWho does it?Responsibilities and encapsulationComponents turn into controllers
ReactiveState, event, derivation, projectionWhat follows?Consistent UI from stateUnfamiliar thinking at first

None of these models is wrong on its own.

The question is: which runtime model am I operating in?

In the backend, a linear flow is often natural. In frontend, it’s often just the happy path. As soon as real UI complexity shows up, states start overlapping.

The user clicks multiple times. Requests run in parallel. Routes change. Components appear and disappear. Validation changes while someone’s typing. Permissions kick in. Loading, error, empty, and success states overlap. Browser history and deep links exist. Rendering happens repeatedly.

So a modern screen is less of a flowchart.

It’s more of a running state machine.

Imperative frontend code often works surprisingly well at first.

The happy path is linear:

Click → Save → Success → Toast → Navigate.

That’s quick to write. It’s easy to follow. It matches experienced OOP reflexes. It gives a feeling of control.

Especially in early project phases, this feels productive. You see results. Features get built. The UI responds. Customers can click things.

And when something doesn’t fit, you just add another flag.

isLoading. isSaving. hasError. showEmptyState. isInitialized. reloadRequired. formWasTouched. manualRefreshNeeded.

That works too, for a while.

Until several things happen at once.

That’s when the real complexity starts: which flags are mutually exclusive? Which request is allowed to overwrite which state? What happens if the user changes the route while saving is in progress? Who resets the error? When is a cache stale? Why does the spinner keep spinning? Why doesn’t the list update? Why does a toast show up twice?

At this point, control turns into coordination.

And coordination is expensive.

Why it fights the framework in the long run

Section titled “Why it fights the framework in the long run”

Angular, React, and Vue differ a lot in APIs, philosophy, and ecosystem.

But they share one basic idea:

Rendering follows from state.

React thinks about UI through state and props. When state changes, a new render follows. Vue works through reactive data, computed values, and template bindings. Angular keeps moving further toward signals, computed values, template bindings, router state, forms state, resources, stores, and clear models of change.

The APIs are different.

The paradigm is similar:

UI is a projection of state.

The frameworks don’t want you to manually synchronize the screen step by step forever. They want you to model states, express dependencies, and describe derivations.

Anyone who coordinates everything themselves instead is working around the framework.

It often looks like this:

The router doesn’t get understood as a navigation model — it gets treated as an annoying mechanism. Forms don’t get understood as a state model — they get treated as input fields with their own error maps. Change detection doesn’t get understood as a rendering model — it gets treated as something you have to manually “nudge.” State management doesn’t get understood as a projection of events and state — it gets treated as a collection of global cache classes.

At that point, you’re only using Angular, React, or Vue as a render layer.

The real runtime model lives in your own code.

The counter-framework inside the framework

Section titled “The counter-framework inside the framework”

The first workaround is rarely the problem. Its career is.

The dangerous part is rarely the first helper function.

A helper is harmless. A workaround is explainable. A small custom abstraction is often reasonable.

But workarounds have careers.

Today it solves a local problem. Tomorrow it becomes an internal pattern. The day after, a second team depends on it. Then conventions form. Then it needs documentation. Then only one person still knows the edge cases. Then the framework upgrade gets hard, because it’s no longer clear which responsibility belongs to the framework and which one the homegrown solution took over.

When the framework router gets replaced by a custom router, forms and validation by custom validation, change detection by custom render synchronization, and state management by cache classes, you’re no longer using Angular as your architecture model.

At that point, Angular is just the render layer of a framework you built yourself.

Angular wasn’t the problem. The problem was the code that kept Angular from being Angular.

That’s the core of it.

Not every framework concept is perfect. Not every API is pretty. Not every recommendation fits every project. But when you replace core concepts, you take over their responsibility.

And that responsibility is bigger than it looks at first.

Routing isn’t just the question:

Which page is visible?

Routing covers deep links, browser history, guards, lazy loading, redirects, parameters, reload behavior, the back button, navigation state, failure cases, and often permissions too.

A custom router often only takes over this responsibility partially. That’s enough for the first click. Enough for the happy path too. But at the latest when reload, browser history, nested routes, guards, or deep links come up, you notice that routing is a runtime model.

A custom router is rarely just a router. It’s the start of a homegrown runtime model.

Sometimes there are good reasons for routing abstractions. For example, to encapsulate business-driven navigation or keep URLs stable. But that abstraction should respect the router, not replace it.

Validation isn’t just:

Is the field empty or not?

Validation connects user input, dirty and touched states, submit eligibility, backend errors, translations, accessibility, and display.

The moment custom validation logic exists alongside form state, you risk having two sources of truth.

The form says: valid. The error map says: invalid. The template shows: maybe. The submit button decides: by its own logic. The backend responds: yet another answer.

Then synchronization begins.

And synchronization is usually a sign that the model has the wrong boundaries.

Validation doesn’t belong in some loose helper class that happens to map strings onto fields. It needs a deliberate model: What comes from user input? What comes from the backend? What’s display? What’s a business rule? What’s a technical format?

Only then does validation become testable and understandable.

Custom change detection is a particularly strong warning sign.

Of course there are special cases. Performance optimizations. Zoneless applications. Manual boundaries. Large lists. External APIs.

But when, in normal everyday feature work, you regularly have to manually make sure the screen “finally updates,” rendering usually isn’t the real problem.

Often the state is modeled wrong.

Or it gets changed around the framework.

Or there are multiple sources of truth.

Or side effects happen in places where derivations should be instead.

Custom change detection is rarely a performance feature. It’s often an architecture symptom.

You can optimize rendering. You can understand change detection and steer it deliberately. But you shouldn’t use it as a band-aid for an unclear state model.

A lot of projects say cache but mean state management.

A cache class starts out harmless:

private items = new Map<string, Item>();

Then selection state gets added. Then loading state. Then error state. Then invalidation. Then reload. Then events. Then side effects. Then global mutable state.

Eventually the class is a store, a repository, an event bus, a cache, and a process manager, all at once.

And nobody calls it that.

That’s dangerous, because a real cache needs a clear invalidation model. When is data valid? Who’s allowed to overwrite it? What happens with parallel requests? What happens on logout? What happens on a tenant switch? What happens after a mutation? What happens on errors?

Without those answers, the cache isn’t a cache.

A cache without a clear invalidation model isn’t a cache. It’s hope with a Map interface.

The same rule applies here: custom abstractions can make sense. But if they replace state management, they should also be designed, tested, and operated like state management.

The cost of these replacement systems rarely shows up on the first feature.

It shows up later.

Major upgrades. New framework paradigms. New team members. Testing. Refactoring. Security requirements. Performance problems. Migrations.

In one concrete project diagnosis, a project was still stuck on Angular 17 in 2026. An analysis found that, with a lot of effort — roughly 60-plus points of work — an upgrade to Angular 19 would probably still have been possible. After that, it would likely have been the end of the road, because the homegrown solutions no longer fit the framework’s modernization path.

That’s not a general rule. That’s a project diagnosis.

But it shows a pattern.

As long as the framework only gets used as a render layer, you lose touch with its ongoing development. New APIs barely help if your own runtime model works against them. Signals don’t help much if the real state sits in mutable cache classes. Better forms don’t help much if validation was built entirely on the side. Router improvements don’t help much if navigation happens in your own system.

The most expensive part of a counter-framework isn’t building it. It’s migrating away from it.

Learning curve and organizational responsibility

Section titled “Learning curve and organizational responsibility”

You don’t learn reactive thinking in one sprint.

That matters to me.

Anyone who spent years training on OOP, services, methods, control flow, and transactions doesn’t just switch their mental model by reading a framework tutorial. Of course you can use a signal API. Of course you can subscribe to an Observable. Of course you can write a React hook.

But that doesn’t yet mean you’re thinking reactively.

Reactive thinking only starts where you grasp states, events, and derivations as an architecture model. Where you no longer ask first, “Which method do I call now?” but instead, “What situation are we actually modeling here?”

From practical experience, with guidance it often takes about six months before developers really start to understand this pattern of thinking instead of just mechanically using the APIs.

That’s not a weakness.

That’s learning.

A lot of organizations don’t give that time, though. Teams are supposed to build modern frontends but don’t get time to learn the paradigm. There’s a new framework but no new understanding of architecture. There are tickets, deadlines, and delivery pressure. So developers fall back on what they already know.

Then workarounds appear. Then workarounds become patterns. Then patterns become architecture. Then architecture blocks the product.

That’s rarely an individual failure.

It’s often an organizational problem.

If companies want modern frontends, they have to take the paradigm shift seriously. Not as a luxury. Not as training for “frontend toy stuff.” But as an architectural foundation.

Replace a core concept, and you take on its entire responsibility.

Of course not every custom abstraction is wrong.

On the contrary: good architecture needs abstractions.

Custom abstractions make sense when they protect business logic, encapsulate external systems, or express recurring use cases.

A good abstraction protects business language. It encapsulates infrastructure. It respects the framework’s paradigm. It reduces coupling. It makes behavior more testable.

It gets problematic when custom abstractions replace core framework concepts without taking on their full responsibility.

Dangerous abstractions are the ones that replace routing. Replace validation. Replace change detection. Replace the state model. Create dual sources of truth. Require insider knowledge. Block upgrades.

The decisive question isn’t:

Am I allowed to build a custom abstraction?

Of course you’re allowed to.

The better question is:

Does this abstraction complement the framework — or does it secretly replace its runtime model?

That’s the difference.

A facade over a store can make sense. A mapper into a ViewModel can make a lot of sense. An anti-corruption layer against external API models is often necessary. A business-driven navigation abstraction can help. A clear application service can express use cases.

But a custom router, custom validation, custom change detection, and global cache classes aren’t small helpers anymore. They’re architecture decisions.

So they should get treated that way.

How to tell you’re fighting the framework

Section titled “How to tell you’re fighting the framework”

A project isn’t fighting its framework just because a helper exists somewhere or a method looks imperative.

The warning signs are different:

When routing no longer works except through internal state. When validation has to rebuild the same truth in three places. When components regularly trigger rendering manually. When cache classes know more about the screen than the ViewModel does. When a framework upgrade fails not because of the APIs but because of homegrown logic that took over framework responsibility.

Then the problem isn’t a single line of code.

Then the project has started building its own runtime model.

Please stick to your framework’s paradigms

Section titled “Please stick to your framework’s paradigms”

That brings us to the actual takeaway:

Please stick to your framework’s paradigms.

Not out of dogma.

Not because Angular, React, or Vue are sacred. Not because framework documentation is always perfect. Not because your own ideas are forbidden.

But because frameworks are runtime models.

Choosing a framework isn’t just choosing syntax. Not just components. Not just build tools. You’re choosing a mental model.

You can learn that mental model. You can deliberately step outside it. You can abstract in specific spots. You can even criticize it.

But you shouldn’t replace it by accident.

If you’re using Angular, React, or Vue, let those frameworks actually do their job.

Model state. Describe derivations. Send intents. Use routing, validation, and rendering the way the framework intends them. Encapsulate business logic, but don’t secretly replace the runtime model.

Everything else feels like control at first.

And in the end, a framework of your own that nobody wanted to build.