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