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