The State Changed. Did the Display?

Have you ever used an application where the window just didn't update? Maybe you had to close a dialog, reload a page, or even restart the whole application, just to see the updated value. Nobody likes doing that. We expect an application to show current data and to update what is shown whenever that data changes. Why not build applications like this?

As a software engineer, this can be a challenge. It can be hard and time-consuming to give your users this level of convenience. It is even harder to do it reliably, without putting a lot of work into development and testing.

This is not a theoretical problem. An analysis of Adobe desktop applications revealed that event‑handling logic contributes to about half of all reported bugs. That is the manual wiring between state changes and display updates.

For a complex application you might end up writing an enormous number of callbacks. They form a tangle of isolated code fragments that each manipulate the same data. They have a hard‑to‑predict execution order and no single entry point to reason about. So we need a deeper understanding to avoid running into such problems.

This article series is designed to help you avoid such problems. I use TypeScript for most of the code examples, but the series explains principles and approaches used in many programming languages and their ecosystems. First, though, let's build a mental model for what the actual problem is.

Application Structures

Most applications maintain internal state that must be reflected in a display of some kind. The state holds the data the program works with — structured objects, numbers, strings, lists, and so on. The display presents that data to the user in a form they can read and interact with. The challenge is ensuring that the display always reflects the current state. Changes in any piece of state must trigger the right update in the view.

This problem is not specific to any platform or framework. It appears the moment an application has both internal state and something that presents it to a user — a GUI window, a TUI region, or any other form of display. The same problem shows up in build tools that watch for file changes, in monitoring systems that alert you when a server misbehaves, and in many other places.

But UI development is likely where most programmers encounter it first, and most intensely. Most of us build a user interface sooner or later. There, the number of state‑to‑display connections grows very fast, and users don't like stale values on their screen.

Patterns like Model‑View‑Controller (MVC), Model‑View‑ViewModel (MVVM), Model‑View‑Update (MVU), or Model‑View‑Presenter (MVP) all have a model in common. That model is the application state at issue here.

structure state Application State display Display state->display state changes display updates display->state user interaction writes back

The display and the application state might each be split into parts that are related to each other. You could plot this as a graph and draw arrows from a part of the application state to the part of the display it affects. An arrow from one part (source) to another part (destination) means that if the source changes, so should the destination. Consider a messenger app. When it receives a message, it adds it to the list of unread messages. A little counter shows the number of unread messages, so it must be updated. There should therefore be an arrow from the list of messages to that counter display. A dependency‑graph could look like this:

structure cluster_2 State cluster_3 Display stateMessageList Message list messageCounter Unread message counter stateMessageList->messageCounter messageList Message list stateMessageList->messageList connectivity Connectivity connectivityIndicator Connectivity indicator connectivity->connectivityIndicator

The challenge is not just making one part of the display track one part of the state. It is keeping the whole dependency‑graph correct over time, and doing so efficiently. It should be easy on the hardware and on the developer, while still delivering high‑quality software reliably. Let's think of a more complex example.

A Concrete Example: The Shopping Cart

Consider an online shopping cart. It has a list of items, each with a quantity and a unit price, a total computed from those, the included taxes, a delivery method with its own cost, and a final total.

The UI for it might look like this:

ArticleQuantityPriceTotal PriceRocking Chair1400400Coffee Mug12020Lamp1100100Bag of Pretzels10330Total price (incl. taxes):550of which taxes:55Select delivery methodStandardDelivery cost:20Total cost (incl. taxes):570Order nowOrder summary

The item table shows four products with their quantities, unit prices, and line‑totals. The order summary on the right shows the total price and the amount of taxes included in that price. It also shows the selected delivery method together with its cost, and the final total cost. Every number on this screen depends on some combination of the others, and when any input changes, every dependent value must update.

If the user changes the quantity of the Bag of Pretzels from 10 to 15, the line‑total for that row changes from 30 to 45. That change ripples to the total price (550 → 565), the amount of included tax (55 → 56.50), and the final total cost (570 → 585). Four display elements change because one input changed. If the user switches from standard to express delivery, the delivery cost changes but the line‑totals, total price, and tax amount do not — because delivery is not part of any of those. Different inputs affect different subsets of the display.

Drawing those dependencies makes the ripple visible. One edited number leads to four UI elements being updated and some intermediate calculations:

cart amber = recomputed when the pretzel quantity changes cluster_1 State cluster_2 Display qty1 quantity (Rocking Chair) totalPrice1 line total (Rocking Chair) qty1->totalPrice1 listDisplay Table of items qty1->listDisplay qty2 quantity (Coffee Mug) totalPrice2 line total (Coffee Mug) qty2->totalPrice2 qty2->listDisplay qty3 quantity (Lamp) totalPrice3 line total (Lamp) qty3->totalPrice3 qty3->listDisplay qty4 quantity (Bag of Pretzels) totalPrice4 line total (Bag of Pretzels) qty4->totalPrice4 qty4->listDisplay price1 price (Rocking Chair) price1->totalPrice1 price1->listDisplay price2 price (Coffee Mug) price2->totalPrice2 price2->listDisplay price3 price (Lamp) price3->totalPrice3 price3->listDisplay price4 price (Bag of Pretzels) price4->totalPrice4 price4->listDisplay deliveryMethod delivery method deliveryMethodDisplay Label: delivery method deliveryMethod->deliveryMethodDisplay deliveryCost delivery cost deliveryMethod->deliveryCost totalPrice total price totalPrice1->totalPrice totalPrice2->totalPrice totalPrice3->totalPrice totalPrice4->totalPrice totalPriceDisplay Label: total price totalPrice->totalPriceDisplay totalTax taxes included totalPrice->totalTax totalCost total cost totalPrice->totalCost totalTaxDisplay Label: taxes included totalTax->totalTaxDisplay deliveryCostDisplay Label: delivery cost deliveryCost->deliveryCostDisplay deliveryCost->totalCost totalCostDisplay Label: total cost totalCost->totalCostDisplay

For this diagram, we ignore the wiring for updating the item table and treat it as one atomic element. In reality we would need to add wiring for each line.

The code to handle a single quantity change might look like this:

function updateQuantity(productId: string, quantity: number) {
cart.items[productId].quantity = quantity;
recalculateLineTotal(productId); // must remember this
recalculateTotalPrice(); // and this
recalculateTaxes(); // and this
recalculateTotal(); // and this
}

In a manual system, every function that modifies the cart — changing a quantity, adding or removing an item, switching the delivery method — must know which downstream values to recalculate and in what order. If you add a discount code field, a loyalty points display, or a gift wrapping option, you must go back and update every function that could affect those new values. The dependency‑graph grows with every feature, and the manual wiring grows with it.

Additionally, the graph changes with every item added or removed, as groups of nodes and edges come and go. Both the state and the display change with each product the user adds to the cart.

Where Do Changes Come From?

Changes to application state do not necessarily originate from a single source. The user might trigger a change by pressing a key, clicking a button, or entering something in an input field. But changes can also come from external events: a file watcher detecting that a directory has been modified on disk, a network request arriving with new data, or a timer firing at a regular interval. In each case, the display must be updated to reflect the new state, regardless of what caused the change.

Can the display update the state? It depends on how you look at it. Many UI widgets combine presenting data to the user and accepting input from the user. They also encapsulate some state that is not relevant for the application. A button might display some text or an image, but it can also be pressed. A text field can display some text, but the user can also edit that text.

To keep things simple, we will split these aspects of the display conceptually. The display is only for outputting data to the user (or some external entity), while changes come from user actions and external events.

sources user User actions state Application State user->state  edits a value   external External events (file, network, timer) external->state  delivers new data   display Display state->display every dependent element updates

To summarize, the challenge is not just making one display element reflect one piece of state. It is building a system where any change, from any source, reliably reaches every display element that depends on it. It should be easy and fast to develop and maintain, and it should be hard to make a mistake. At the same time you want the program to use as little CPU and RAM as possible.

You need some kind of framework for this. That could be a library you pull into your program, or simply a particular way of structuring your code.

The Three Design Goals

When designing a system where the display reflects the state, three desirable properties recur across frameworks, languages, and platforms. They are the conditions that make such a system maintainable as it grows.

Encapsulation

The display code should not need to know how the state is structured internally, and the state code should know as little as possible about how the display works or that it even exists. Each side should expose a narrow interface that the other side depends on, without reaching into each other's internals. This is the same principle of modularity that applies to any well-designed system: modules communicate through defined contracts, not based on shared implementation details.

Compatibility

The mechanism that connects state to display should not require you to abandon the programming style you already use. Functions, variables, loops, and conditionals should still work. If a framework forces you to write everything in a special way — with a different lifecycle, a different mental model, or a different set of rules — that costs you in multiple ways:

  • you and your team have to learn the new way of writing code
  • you need to modify your old code, possibly introducing new bugs
  • you might become bound to one framework, making a switch costly

The framework should adapt to you, not the other way around.

This design goal comes in different levels. You might be able to use your compiled code as it is. You might be able to keep your source code, but change the compilation process slightly. Something might change the way your code is executed. Or you might have to change your source code only a bit.

Locality

When you change one thing, you should not have to change five other things. As the application grows, the connections between state and display multiply. If adding a field to your data model means touching display code in three different files, the system has poor locality — and every such change becomes a little coordination problem. Of course it's even worse if this increases with the growth of your application. Having poor locality can create a lot of maintenance friction and bring your project to a halt after you've already invested a good amount of resources into it.

The weaker but still valuable form of this goal is that you should not need to know about five other things in order to make a change safely. For example, maybe you start making a change in one place of your application. If this leads to compiler errors which force you to update all other places, that's not so convenient, but still better than demanding that you yourself think of the other places to change.

Where Do We Go Now?

There are only a few fundamentally different approaches to the question of how the display learns that the state changed. Each approach has trade-offs in complexity, performance, and mental overhead.

This series walks through these approaches in order, starting with the simplest and building toward the more sophisticated. Each article introduces one strategy, examines how it works, and considers when it is the right choice and when it falls short. The examples use UIs because that is where the problem is most visible, but the same concepts apply to CLI tools, data pipelines, and any system where internal state must be reflected in an output. The goal is not to advocate for any particular framework, but to build a vocabulary and a mental model that apply regardless of which technology you use.

Suppose you want to start a new application project and you need to choose the technology to code your UI. You will want to pick some framework. Many frameworks will give you a minimal example of how to use them. Choosing one only based on those examples can be deceptive since these are minimal examples. Your application might become quite large and that's where the framework has to shine.

This article series helps you think about how each approach scales, and how the choice will influence your development costs. Making a good choice requires understanding the problem, not just comparing popularity.

Tip
No prior experience with display programming is required. Every concept is explained from scratch. Most code examples are in TypeScript, but the ideas apply to any language.

The next article examines the two simplest answers: the game loop, which rebuilds the display a fixed number of times per second, and polling, which reads a value on a schedule and acts only when it changed.