Views

After the data-model is updated via a Reducer, all views connected with the lager::watch() function are called. There are various ways to implement a view in the Lager model.

Value based UI

In an ideal world, the view is just a value and the user interface is a function that takes a model and returns such view. This fits perfectly the unidirectional data-flow architecture.

However, to do make this pattern convenient and efficient you need a supporting framework. In the JavaScript world, virtual DOM frameworks like React allow us to conveniently represent the UI as a value, and use this declarative description to efficiently manipulate an underlying UI object tree. Sadly, in the C++ world, no such library exists.

This approach is therefore not practical for Lager applications at the moment.

Support the development of a value-based UI library for C++!

Thanks to value semantics and static polymorphism, C++ would indeed be a great language for a React-like library. We at Sinusoidal Engineering spend a lot of time thinking about how to further value-oriented design in C++, and have quite a bunch of ideas on how to build a declarative UI framework, one that does not reinvent the wheel and can be plugged on top of existing native frameworks like Gtk+ or Qt, Cocoa or the Windows API.

This is however a non trivial project. Help us save the planet: no more inefficient Electron apps! Please consider sponsoring that project so that we can invest the time required to develop it, and also tailor it to your particular needs. Contact us to make it happen!

Immediate mode UI

A practical alternative is the usage of immediate mode UI. There are a few UI libraries following this approach and they are specially popular for the development of video games. These include ImGUI and FlatUI.

In an immediate mode UI, whenever you need to redraw, you traverse your whole state tree invoking functions of the framework as you go to represent it, drawing buttons, input boxes, labels, windows, etc. The architecture of a Lager application fits this model very well: our state is composed of simple value types that are easy and fast to traverse.

Furthermore, using Lager helps solve some of the problems that immediate mode UI’s have when scaling. One is state tearing. In a naive immediate mode UI, the state is mutated as you draw it, often directly by the framework primitives. This means that if the same piece of state is represented in multiple places, it might be inconsistent in a frame, as the mutation happens during the rendering. When using Lager with an immediate mode UI framework, you can not mutate the state directly, but dispatch actions to trigger a state change cascade. Lager works in a transactional way, making all mutations traceable to the actions that triggered them, and ensuring that always consistent snapshots are rendered.

Exercise

Write an ImGUI interface for the todo-list app core core we described before. If you want it to be reviewed and potentially included as an offical Lager example, make a pull request to the project.

Widget tree UI

Most C++ UI frameworks, like Gtk+ or Qt, are based on a widget tree represented by mutable objects, that notify events through listeners.

These are designed around the assumption that your model is also a mutable object tree, that you can observe via listeners. Whenever the model is changed, a listener manipulates the widget tree to keep it updated. Whenever the user manipulates a widget, another listener manipulates the model. This is the kind of circular relationship that we wanted to avoid with the unidirectional data-flow.

We can still use an unidirectional data-flow approach in combination with a widget tree UI, by breaking the circle as follows:

  • We connect listeners to the widgets as usual to observe user interactions, but they do not mutate the model directly. Instead, they dispatch actions.
  • There are no listeners connected to the model, that is indeed impossible, because the model is a value type. But the model can be easily copied, so we can diff it. We keep a copy of the last version around, such that when the new state of the application is pushed, we traverse the state, comparing the old and new values. When discrepancies are found, we update the widget tree in the same way we would do inside a listener.

Library support

This diffing mechanism can be a bit cumbersome, and sometimes error prone. Cursors can do it for you automatically. They can also do much more, and are an invaluable tool when interfacing a value-oriented data model with an object-oriented UI<https://www.youtube.com/watch?v=e2-FRFEx8CA>.

Observables

Another way to look at the state of a Lager application is as a sequence of values over time. Leveraging this realisation, we can apply the reactive programming paradigm to manipulate it.

The RxCpp library is precisely designed to work with sequences of values that change over time. These sequences can be reified as values called observables that can be manipulated using higher order transformations and scheduling combinators. We can use the view function that is passed to the store to push these into an Rx observable. This is then used to feed other subsystems in a reactive manner. We can also use Rx observables to source the actions into the store.