Building a SAPUI5 application with Predictable State Container
Building large web applications can get complex pretty fast. Especially managing the state within the application is not always easy. Which buttons or fields should I show based on the users previous actions? Where do I get the data from that is currently displayed? Questions like this and others will arise during the development process.
Currently SAPUI5 offers different models to store data (OData, JSON, XML). These models use two-way-binding to update properties and objects. However, this can get complex if you get your data from different models, you have controllers update data, but controls can update data as well. In such a non-deterministic system it can get increasingly difficult to reproduce errors or add new features.
This can be changed by using a predictable state container like Redux. Redux attempts to make state mutations predictable by imposing certain restrictions on how and when updates can happen. These restrictions are reflected in the three principles of Redux. The Redux documentation gives a pretty good overview (just follow the link).
The three principles are:
- Single source of truth: The state of your whole application is stored in an object tree within a single store.
- State is read-only: The only way to change the state is to emit an action, an object describing what happened.
- Changes are made with pure functions: To specify how the state tree is transformed by actions, you write pure reducers.
So how can we integrate this concept in SAPUI5? I created a custom One-Way model implementation (ReduxModel) which builds the bridge between Redux and SAPUI5 applications. It works similar to the UI5 JSON Model so you can use the same syntax to bind properties.
Installing the ReduxModel:
This guide assumes that you build your application as described in the openui5-sample-app, using Grunt and Bower to manage dependencies and build the application. The process of including the new library might be different if you consume UI5 in a different way.
First you need to install the redux model and add it to your bower dependencies:
bower install openui5-redux-model --save
Make sure to also update the settings of the server you are using so the files are included in the resources folder.
Then you can add the redux model as an additional dependency in the SAPUI5 bootstrap tag:
<script id="sap-ui-bootstrap"
src="resources/sap-ui-core.js"
data-sap-ui-libs="redux">
</script>
You also need to integrate redux somewhere in your application. It is not included with this library. You can do this by loading it from a CDN via a <script> tag or by including the redux.js file directly somewhere in your application. By default redux comes as a AMD module which makes it also available in a global variable. However, you can also include it via the UI5 module system. To do that you need to register it with the UI5 core.
jQuery.sap.registerModuleShims({
'app/thirdparty/redux.min': {
exports: 'Redux'
}
});
Setting up the model
First we need to create a redux store and add one or more reducers to it. You can find more information about this in the redux documentation. It makes sense to create a separate module for the redux store, so you can require it in different parts of the application (for example to dispatch actions in a controller). In this example I also included the redux logger middleware, which makes it possible to see all redux actions logged in the console.
sap.ui.define([
'../reducers/index',
'../ext/redux.min',
'../ext/redux-logger.min',
], function(fnReducers, Redux, createLogger) {
var oStore = Redux.createStore(
fnReducers,
Redux.applyMiddleware(
createLogger()
)
);
return oStore;
});
Wherever you setup your model you can require the store and instantiate the ReduxModel.
sap.ui.define([
'redux/ReduxModel',
'./store',
], function(ReduxModel, oStore) {
return {
createReduxModel: function() {
const model = new ReduxModel(oStore);
return model;
}
}
});
Setup Bindings
Bindings can be setup the same way like you are used to from other models in SAPUI5. If you have a reducer foo which has a property bar you can just define a binding as simple as:
<Button text="{redux>/foo/bar}" />
Dispatching actions
Because of the fact that the redux model is a one-way model you cannot update bindings by calling setValue or similar functions. You have to dispatch actions, which are then processed by the reducers to update the state. After that all bindings will be updated (if they have changed).
For this example we have a controller App.controller.js which implements a function handleButtonClick that should trigger a state change of the application.
sap.ui.define([
'sap/ui/core/mvc/Controller',
'../store'
], function (Controller, oStore) {
Controller.extend('my.controller.App', {
handleButtonClick: function() {
oStore.dispatch({
type: 'MY_ACTION',
meta: {},
payload: {}
});
}
});
});
Selectors
It is not a best practice to access data in your store directly because the structure of how your data is stored can easily change as you develop your application. The better approach is to use selectors with a defined API.
Redux tip: export selectors with related reducers. They let you decouple views and action creators from state shape. pic.twitter.com/SxRhOztcaB
— Dan Abramov (@dan_abramov) November 11, 2015
Selectors are also supported by the ReduxModel. You have to use a special syntax in the binding to access them. All selector binding must start with /selector
. The second part of the binding in the class in which the selector is defined. It will be loaded via the UI5 module system.
<Button text="{/selector/my.selectors.SelectorClass/selectorFunction}" />
You can specify a prefix for all bindings as second argument when you create the ReduxModel.
var oStore = Redux.createStore(fnReducer);
var oModel = new ReduxModel(oStore, 'my.selectors');
sap.ui.getCore().setModel(oModel);
By doing that you can leave out the prefix when you define the binding:
<Button text="{/selector/SelectorClass/selectorFunction}" />
All selector functions will receive the current state as a first and an optional context as second argument.
Summary
The blog post shows how to integrate a predictable state container in a SAPUI5 application. By doing so, there is only one central place where all data which is required to render the application is stored. This makes it much easier to handle data within the application and also enables easier testing and problem solving.
Please share your thoughts if you find this post helpful and if the ReduxModel can be further improved.
Hi Christoph,
When I saw Christian's blog post about MobX I wondered if there is a solution with Redux. I'm familiar with React/Redux and I am curious about MobX. Do you have ideas about how they compare in OpenUI5 context?
Thank you for sharing the library and blog post!
Hi Faith,
I guess both approaches bring some kind of state management to UI5. I have not used MobX so I cannot really say anything about which is better suited. I guess the main difference is that Redux stores the state in one big immutable object, while MobX takes an object oriented approach to store the data.
I just found this blog post. It might help to understand the differences better: https://www.robinwieruch.de/redux-mobx-confusion/
Haha nice to see this article and implementation and big thanks to @Christoph Kraemer for it. Now we have support for basically the two most popular state management solutions for js
. (for MobX implementation refer to MobxModel)
@Fatih I wasn’t aware of Christoph’s efforts, but I can give some personal (opinionated) thoughts on my experience with Redux and the difference between Redux and MobX.
First of all let me clearly say that I personally prefer MobX over Redux, independent of the context (be it openui5 or react). In general I think most of the pros and cons of MobX vs Redux apply equally to React and openUI5, so you can probably read any other (non-ui5) blog post about it.
Whats the gist of Mobx vs Redux?
Redux uses an immutable state tree and one produces “new versions” of that state tree through pure functions (reducers) while using structural sharing between old and new versions of that state tree. In general it is praised for it’s predictable and enforced one-way data-flow and the natural ability to perform time travel debugging (switching to old versions of the state tree etc.).
Mobx instead uses a mutable state-tree consisting of “observable” objects. Those objects can be plain objects or instances of classes with methods. The unique feature of mobx is that you can define computed properties (sort of like views) on your state tree which are automatically updated once you mutate your state tree (and with the least number of computations possible). One big difference is in mobx you can modify / mutate your state tree directly (for example in a button’s event handler) or use things like two-way data binding and the derived values are still updated. While this is convenient when you start it can become quite messy in a big project and you lose “track” of all the places you mutate your state. That’s why mobx also has some means to enforce that your state is only changed through defined methods, called “actions” which serve a similar purpose to redux reducers. This can even be enforced on a technical level by enabling mobx’ “strict” mode. So basically you can create a redux like one-way data flow, but for the start you can also mutate the state directly (hence it’s less opinionated).
Personal Experience
Redux
I’ve used Redux together with React for a week on a personal learning project about 1.5 years ago and was initially very excited about it’s concepts, especially the natural ability to use time-traveling and the fact that it embraces functional programming principles. Even before I saw Redux (and still today) I used a lot of functional style js (.map .filter and all kind of things from the haskell inspired library ramda).
However retrospectively I must say I’ve “got very little stuff done” in this week of redux and the reason for this are nowadays my 2 main concerns with Redux:
1. The learning curve is generally higher (even when you come from a functional programming background, not to speak of explaining that stuff to your colleague who doesn’t have any prior exposure to FP).
2. Redux in almost all use cases requires you to write more boilerplate. Let’s say you add for example a single button to do “something” you would have to create an action type, action creator, reducer, possibly a selector and eventually add the selector in your ui5 view. Even worse (as per best practices 1.5 years ago) you would do all those changes in different files which requires a lot of mental context switching.
While the first point you’re able to overcome after a few days, the second point would persist forever and really annoyed me, as I strongly have a preference for lesser number of lines of code to get things done. In some cases (not all) it can happen that a mobx equivalent app has only half as much code as it’s redux equivalent.
I would say those 2 concerns are generally accepted from both sides in redux vs mobx discussions, but their impact and relevance (vs other factors) is seen differently. Most Redux advocates perceive the opinionated app structure / boilerplate as a good thing, because it enforces a certain uniform structure and accepted best practices useful in a team context. For me less code (and getting things done) is the more important factor, even if I have ensure “best practices” manually.
MobX
MobX itself I discovered about half a year later than Redux. Even though I was initially less excited (compared to Redux) I basically understood most of it’s concepts and best practices within few hours and was able to write functional code with it. (Btw this interactive 10 min tutorial is the first thing I basically tried out back then: https://mobx.js.org/getting-started.html). (Disclaimer: I had some prior exposure to RxJS and Object.observe, so I already understood the basic concept of Observables).
While I’ve known both libraries for more than over a year I only applied MobX recently in a real world project. Reading then again about MobX best practices I was initially a bit irritated about creating object oriented domain classes etc. (because I prefer in general functional constructs) but I must say it just worked like a charm and is easy to reason about. Also more than 70% of my domain class code is basically computed properties which are side-effect free and hence relatively easy to test and reason about.
My use case with MobX (and why Redux wouldn't have sufficed)
The use case I applied it was basically a couple of reuse controls for form-heavy configuration UI’s of different micro-services. While those micro-services all fulfill a different purpose, they have common RESTful Configuration API, which accepts a loosely structured JSON graph as payload. The most important was a reusable SplitApp control which manages the whole REST communication to a backend, displays a list of different configuration variants, conditionally enables save, copy , delete etc. buttons. This SplitApp control is used for the configuration UI’s of 3 different micro services (and in very near future 7+) and offers an UI extension point to define the service specific form-content and validation. Using this control other team members would just define the service specific form content and validations but the control takes care of doing all the REST communication, manage save, delete etc. actions. Last but not least an I18nManager control which renders dynamically a form of i18n (language) texts to be entered by a user. The set of fields this I18nManager form contains itself depends on some values the user enters in the service specific / extension form (so the I18nManager form content is partially derived from the data in other forms).
Now in this project I implemented those reuse controls but other developers implemented the service specific form content and validations (using the extension point). A lot of the values (e.g. if a button is active) in the SplitApp and the I18nManager depend on what the user entered in the service specific form content or if he did a change in a field at all. So basically there is shared state between the SplitApp, I18nManager and the extension content. In practice the SplitApp would create a MobxModel and bind it against itself, the I18nManager and against the form content in the extension point. The developer of the extension point would basically create a view (js, xml whatever he likes) with the form content and a controller to do some specific validations. He also would be able to access the shared state model that was previously created by the SplitApp and modify the values inside it. The SplitApp then can transparently observe all changes the extension point form did to the model and render all dependent things and if the user clicks the save button send it to the backend etc.. Now because mobx is so lightweight and unopinionated the extension point developer can modify the model however he wants (he can also use model.setProperty), even with 2 way data binding, but the SplitApp still reacts accordingly. Even though I used domain classes inside the SplitApp for many parts, the extension developer didn’t have to introduce domain classes for his form state, but simply use plain objects. He could actually use the model almost exactly like JSONModel – no need to implement reducers, special selector binding syntax etc.. In fact the developers who implemented the extension points didn’t have to know anything about mobx at all – for them the whole thing just appeared like a standard JSONModel. All of the extension point implementations don’t even contain the word mobx, even though they implicitly access and use it.
I really can’t imagine that this would’ve been possible with Redux. Using Redux would’ve implied that every extension point developer – which includes even devs from customers because we ship an SDK with those controls – would’ve had to learn and understand Redux to modify the shared state between the reuse control and the extension point. This would’ve cost valuable time and raised a lot of resistance within my team and the customers’. Besides it would’ve been more code for me and the extension point developers.
IMHO the dilemma with Redux is this: If you’re just one developer all the boilerplate and “structure” (that’s supposedly good for teams) slows you down, but if you’re in a team you have to convince all your team members (including your customer’s devs) to spend a significant amount of time on learning Redux. The latter is an important factor to consider if you wanna apply Redux within an openUI5 context where most devs are not familiar with it.
With MobX I didn’t face any of those problems. In fact there are probably a couple of existing ui5 projects where you can introduce MobxModel initially as a drop-in replacement for JSONModel without any further code change.
What others say
Here are some of the resources which I find to be useful for comparison of mobx vs redux:
https://www.youtube.com/watch?v=83v8cdvGfeA
https://github.com/mobxjs/mobx/issues/199 (note that this a thread in the mobx github repo, but the Redux author Dan Abramov himself calls it useful for comparing mobx vs redux: https://twitter.com/dan_abramov/status/734711821295362049?lang=en)
Alright, I hope this gave you some valuable insights, but there’s a ton more resources for mobx vs redux (and beyond) out there.
FYI If you’re missing the time travel from Redux in mobx, I can recommend you to checkout https://github.com/mobxjs/mobx-state-tree which is the latest addition of the mobx’ author. He explains it’s concepts and theories in this video: https://www.youtube.com/watch?v=etnPDw5PKqg&feature=youtu.be&t=32m15s
Hi Christian,
Thank you for the detailed response and personal point of view. I appreciate your effort. I will look closer into the libraries.
FYI, I just read again this article https://hackernoon.com/the-fundamental-principles-behind-mobx-7a725f71f3e8#.q8ksftyw9 which explains very well how MobX still achieves a great level of predictability despite it’s magic appearance.
Very useful information for javascript development, thanks!
Excellent post, both your and Christian's posts pointed me in a new direction. After reading the Redux documentation and watching a short YT video on MobX vs Redux, I will take some time to further familiarize myself with the concepts behind the frameworks.
Do you both have some experience using MobX or Redux in larger business apps carrying lots of data?
Thanks a lot!
Hi Kay,
I use Redux in a large (react) app I build outside of work. It made managing the applications state much easier and the application is much more structured, because you know where you have to change or extend the application when you fix bugs or create new functionality.
@Kay as described above I’ve used MobX in a real world project with good complexity at SAP, however with “limited amount” of data. What I yet have to see is how MobX performs with for example with large and paginated tables or to visualize time-series data, but after understanding MobX’ concepts I’m optimistic it would perform well (and probably better than JSONModel).