5 reasons to avoid imperative code

Mike Pearson - Nov 28 '22 - - Dev Community

YouTube

1. Inconsistent state

In an imperative app, events trigger event handlers, which are containers of imperative code. This imperative code is burdened with the responsibility of understanding and controlling features that often have nothing to do with the original event. Imperative code is written outside the context of the state it's controlling and all the other code that controls that state, which is scattered across other event handlers. This often results in naive update logic that fails to consider the whole picture or account for other downstream effects. This usually causes bugs from inconsistent state.

For example: This is a movies app with a search bar in the header. When you enter a search in, it calls an API and takes you to a page with the search results returned from the API.

Search Results

Many of you are already in the habit of programming this reactively: Just change the URL and let the search results page contain its own logic that reacts to the URL by fetching the search results requested.

But this feature was structured more imperatively, with the search submit handler being responsible for both fetching the results and redirecting.

This caused a bug where if you were on the search results page and refreshed, there would be no search results, because that feature was unable to react to the URL correctly by itself.

inconsistent-state-imperative

Imperative code is buggy by default: If any unforeseen event triggers a state change, downstream state will remain stale and inconsistent.

On the other hand, when downstream state is reactive, it doesn't matter what caused the state change above it. It will always react.

inconsistent-state-reactive

If you think setting the URL first and handling the API request on the route page is a special case, you simply haven't noticed yet that imperative code is the cause of almost all inconsistent state. Hunt down the origin of every bug and you'll find that imperative code can account for as much as 40% of the bugs.

2. State elevation

The movie search feature illustrates another problem with imperative code: State has to be elevated to a level where the imperative code from other features can control it. If event handlers need access to state from other features, that state cannot be local; it has to be global.

State Elevation—Imperative

This is where the original developer used NgRx/Component-Store. He created a global service/store that movie search submit handler could access and store the results of the API call.

The first issue with this is that it decoupled the search results state from the lifecycle of the component. If the search results observable was only used in the component that needed to react to it with the async pipe, it would have unsubscribed from that observable when navigating away. This would have either canceled the API request, or when revisiting the route it would have created a new request.

State Elevation—Reactive

Instead, there had to be extra logic to reset the search results, and in-flight requests were just left un-cancelled.

The second issue with this is that state accessible to every feature is potentially being changed by every feature, and you won't know until you check.

3. Large bundles

The rule of reactivity is that everything that can be downstream should be downstream. This is naturally lazy behavior.

The only framework I know of that makes taking advantage of this easy is Qwik. But even in Angular, it's easy to take advantage of code that's reactive to URL changes, because routes are very easy to lazy-load.

Unless you're using Qwik, every lazy-loaded code bundle probably contains the code of all of its event handlers. Since event handlers are often busy referencing and changing other features, this means those other features need to be included in this bundle too.

Bundle Sizes Imperative

So, if you can, try to have event handlers change the URL and have everything else react to that.

Bundle Sizes Reactive

4. Unnecessary complexity

State that's controlled externally by callback functions relies on those functions to control it in every scenario. This decentralization of control creates duplicated control. In this diagram from earlier, you can see that every new event will require 2 more arrows:

inconsistent-state-imperative

Every arrow that causes State Change 1 must be duplicated for State Change 2. This bloats the codebase.

But does the imperative code really have to be this way? Maybe we can improve it. Let's look at an example and try to make it DRY.

In this example, we'll have carModel as our State 1, and various derived states. We could create a central function for updating carModel called changeCarModel that includes an update to the derived states.

  changeCarModel(newCarModel: CarModel) {
    this.carModel = newCarModel;
    this.zeroToSixtyTime = this.zeroToSixtyTimes[newCarModel];
    this.price = this.prices[newCarModel];
    this.electricRange = this.ranges[newCarModel];
    this.canDriveLaToFresno = this.electricRange > 270 ? 'Yes' : 'No';
  }
Enter fullscreen mode Exit fullscreen mode

One of the benefits of reactive code is that the update logic is next to the state it's relevant to. Can we achieve this with imperative code too? We already have a function for changing carModel right next to it, so can't we just extract the downstream state change logic to individual functions that live next to those other states themselves too? Then we can just invoke those functions from the end of changeCarModel. Let's define the update functions as arrow functions so we can put them right next to their states:

  carModel = 'Chevy Volt' as CarModel;
  changeCarModel = (newCarModel: CarModel) => {
    this.carModel = newCarModel;
    this.changeZeroToSixtyTime(newCarModel);
    this.changePrice(newCarModel);
    this.changeElectricRange(newCarModel);
  };

  changeZeroToSixtyTime = (newCarModel: CarModel) => {
    this.zeroToSixtyTime = this.zeroToSixtyTimes[newCarModel];
  };
  zeroToSixtyTime = this.zeroToSixtyTimes[this.carModel];

  changePrice = (newCarModel: CarModel) => {
    this.price = this.prices[newCarModel];
  };
  price = this.prices[this.carModel];

  changeElectricRange = (newCarModel: CarModel) => {
    const newElectricRange = this.ranges[newCarModel];
    this.electricRange = newElectricRange;
    this.changeCanDriveLaToFresno(newElectricRange);
  };
  electricRange = this.ranges[this.carModel];

  changeCanDriveLaToFresno = (newElectricRange: number) => {
    this.canDriveLaToFresno = newElectricRange > 270 ? 'Yes' : 'No';
  };
  canDriveLaToFresno = this.electricRange > 270 ? 'Yes' : 'No';
Enter fullscreen mode Exit fullscreen mode

Look at that! changeCarModel no longer needs to worry about canDriveLaToFresno, because changeElectricRange calls that update function on its own! And this is all imperative code! Everything follows this pattern:

updateDerivedState = (aboveState: AboveState) => {
  // Update this.derivedState
  // Call update functions for states that derive from this state
}
derivedState = initialDerivedState;
Enter fullscreen mode Exit fullscreen mode

But you know what, there's sort of a circular dependency going on here. The derived state has to know about the state it's derived from, because it wouldn't exist in the first place if it weren't for the state it uses to derive itself. But the top-level state function needs to know about and directly call the derived state update functions below. What can we do about this?

Derived state knowing about state it's derived from reflects the natural relationship between the state and derived state. It's similar to how vanilla JavaScript works when you declare initial values for variables:

const a = 5;
const b = a * 10;
// Notice that `a`'s declaration includes nothing about `b`.
Enter fullscreen mode Exit fullscreen mode

So it seems like the odd part of the circular dependency we've created is that our top-level state has to reference the state change functions below it. Can we get rid of that?

What if we came up with a generic mechanism the top-level state could use to update its derived states without actually referencing them? We could define an empty array that derived states can add their update functions to. The central changeCarModel function could just loop through the array, calling each function with its new value for carModel. This would allow any derived state to just add its update function to that array and have it get called whenever changeCarModel runs. Here's what that looks like:

  carModel = 'Chevy Volt' as CarModel;
  carModelUpdates = [] as ((newCarModel: CarModel) => void)[];
  changeCarModel = (newCarModel: CarModel) => {
    this.carModel = newCarModel;
    this.carModelUpdates.forEach((update) => update(newCarModel));
  };

 // Assign dummy property just so we can run this code here
  dummyProp1 = this.carModelUpdates.push((newCarModel: CarModel) => {
    this.zeroToSixtyTime = this.zeroToSixtyTimes[newCarModel];
  });
  zeroToSixtyTime = this.zeroToSixtyTimes[this.carModel];

  // etc...
Enter fullscreen mode Exit fullscreen mode

Guess what? We basically just invented observables. Those update functions are like observers in RxJS.

Let's refactor completely to observables and compare it with what we just came up with:

Imperative to RxJS Diff

Pretty similar, but RxJS abstracted away the subscription mechanisms so all that's left is our business logic. And it's even more DRY now that initial values aren't treated as a special case.

So how did we get here?

  1. Make imperative code more DRY by centralizing state change code next to state
  2. Achieve the same thing for derived state while also improving separation of concerns, by moving derived state's update logic next to the derived state
  3. Remove pseudo-circular dependencies between state and derived state and improve separation of concerns further by making state agnostic about state that is derived from it
  4. Make code even more DRY by using RxJS

Every step that improves imperative code moves it closer to reactivity. This means the only way to fundamentally improve imperative code is to have less of it.

Let's review the basic rule of reactivity again: Everything that can be downstream should be downstream. This goal is the exact same as that 4-step process we just went through, and it is the best way to get as minimal and simple state management as possible. Downstreaming improves code at every stage.

inconsistent-state-reactive

And since reactive code is located next to the feature it controls, logically similar code will be grouped together, so it's easy to find opportunities to condense repeated logic. This is much harder with scattered, imperative code. In the app I refactored, the first event handler had these two imperative commands next to each other in an event handler:

    this.store.switchFlag(false);
    this.router.navigate(['/']);
Enter fullscreen mode Exit fullscreen mode

At first I had no way of knowing that switchFlag was only ever called at the same time.

After I had refactored to reactivity, it became obvious that store.flag was downstream from the app's URL; it was a flag to determine whether the app was supposed to be on a certain route. You might think this is a ridiculous example, but it's real code that was written, and I really couldn't see how to simplify it until I made it reactive.

Clean code is reactive code.


Here's the repository for the example in this section. And for fun, here's the diff when I convert the RxJS version to StateAdapt, the state management library I created:

RxJS vs StateAdapt

Bad function names

Functions are things that do stuff. That's what they are. So their names should reflect that, right? But if you saw this in a template, could you tell me what it does?

(click)="onClick()"
Enter fullscreen mode Exit fullscreen mode

No. Rather than being named for what it is, this event handler, like most tend to be, is named for how or when it is called.

I know this is extremely common, so at this point I'll cite an authority that might trump tradition. From the classic and extremely authoritative book Clean Code:

Methods should have verb or verb phrase names like postPayment, deletePage or save.

Is onClick a verb? Does it describe what the function is or does? No. It describes an instance in time, when a thing is clicked. All you know when you see a function called onClick is that it is called... on click.

Wouldn't a function name like this be easier to understand?

(click)="refreshData()"
Enter fullscreen mode Exit fullscreen mode

Hopefully you agree with me that that is a better name than onClick.

Alright. Can you help me come up with a better name for this function than onSubmit?

onSubmit function

This is color-coded according to what state is being affected.

Can you come up with a single name that describes what this function does? Here's my best attempt:

ifValidDeleteMoviesFetchAndSaveMoviesAndSaveSearchHeaderFlagAndNavigateElseShowAlert() {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

That is a function name that almost describes everything happening in the function. But it's long. Maybe we can shorten it by extracting commonality from multiple steps and describing the common effect instead of every detail:

validateAndClearSearchAndSaveNewSearchDataAndNavigate() {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Now we could become philosophers and eventually come up with something that describes the overall essence of what's happening, but that would take a lot of time. So it's easy to understand why we're always naming event handlers by when they get called instead of by what they do.

Another technique I've seen is to just pick our favorite behavior from the function and simply ignore everything else:

  navigateBack() {
    this.store.deleteMovies();
    this.store.switchFlag(false);
    this.router.navigate(['/']);
  }
Enter fullscreen mode Exit fullscreen mode

navigateBack? There seems to be more going on there.

Again, this isn't the original developer's fault.

The problem is that all these things need to be done, but they have little in common with each other.

The problem is imperative code.

In an imperative app, events trigger event handlers, which are containers of imperative code. This imperative code is burdened with the responsibility of understanding and controlling features that often have nothing to do with the original event. The event handler requires an umbrella name that covers everything inside it. This can be so difficult that the best name for the function truly is something hollow, like onSubmit.

Reactive code doesn't have this problem.

I refactored that onSubmit code to be reactive and here is what some of the code became:

URL Observable

Do you know how hard it was to come up with the name url$? Not hard at all. Because that's what it is. It might change over time, and it might be derived using other observables, but it still is just an observable of the URL.

Reactive code is declarative, meaning, it declares all its own behavior instead of relying on other code to control it. When no code is reliant on other code to control it, everything can just be concerned with itself only. Nothing is burdened with downstream effects or concerns, so you never have to come up with names that include downstream effects or concerns.

Conclusion

There are many reasons to program reactively. This is why the history of web development has followed an overall trend towards more and more reactivity.

But it's taking a long time. Most developers are still confused about what reactivity even is. Some think that synchronous reactivity covers reactivity completely, forgetting that asynchronous code can also be reactive with tools like RxJS and React Query.

So why is it taking developers so long to get on board with fully reactive programming?

I think it's because JavaScript developers are trained to code with an imperative mindset, because JavaScript 101 includes almost exclusively imperative APIs. I used to think all new developers should start with JavaScript 101, but I'm having second thoughts about that... What would a reactivity-first tutorial look like?

I also think it's hard to stray too far from the dominant style in the ecosystem you work in. For example, if you're using Angular Material and you notice that RxJS is tricky to work with whenever you use a dialog component, you might mistakenly blame RxJS for the bad experience instead of the outdated API for MatDialog.

For most developers, programming reactively will require a big shift in mindset. It will also require some tools and wrapper components.

But every feature I've refactored to reactivity has become simpler, less buggy and more performant.

So I think it's worth it.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player