In this blog I will explain why t-matix has decided to switch the state management system used in the application.
The t-matix platform is an IoT application which enables users to manage their ‘fleet’ of devices, create different features that can be mapped to the output sent by the devices, generate reports based on the data received, and much more. A state management system is an integral part of the application, since the flow of the app is depending on various resources consumed from different API’s. It is important that the data gathered from various endpoints is persistent, easy to access throughout the application, and most importantly, that it can be transformed in any shape needed by the business logic of the application. The latter has been under the influence of organic growth. When it was first developed, it utilized the features of, at the time, the most popular state management system built for React – Redux.
In the course of this year, we developed the idea to reduce the load of the application, narrow down the currently large dependency list, and switch to a different state management system, mostly due to the fact that, in large applications specifically, Redux can become very hard to maintain. Also, an addition of new modules (specific parts of the application responsible for narrowly specified business logic to provide new functionalities currently not existing in the application) shows a lot of ‘boilerplatiness’. In addition (mostly due to legacy reasons), all independent routes (or modules, as previously stated) are served by Django framework, which is the main source of data used as application building blocks. Also, recent migration to the newer version of React (15.x to 16.x) introduced hooks, and in that way motivated the developers to switch from writing class components to currently more popular functional components.
That is why we had decided to integrate RxJS as our main state management system. Although on paper it includes a lot of overhead (in the current state, RxJS offers over 250 different operators), this integration drastically reduces the number of dependencies in the project and switches the flow of data in the application from triggering actions. This causes side effects, the result of which is passed to the pure reducer function, which then creates a new state object populated with the said result, to the stream-like behavior that reactive programming library offers.
Why we used Redux
How we used Redux
Usually, data in our platform comes from different endpoints, services, or API’s. Before it can be used to, for example, build a certain UI interface, or combine data to create a necessary request payload, it has to be transformed. In this example, I will demonstrate how we gather resources from different endpoints, store them in the Redux state, and then modify them to create a data structure that we need. Using Redux, those different resources can be accessed and modified following a simple but very important lifecycle. The ‘Redux’ way of handling the data flow is quite simple in its nature.
First, we instantiate an instance of the store, which contains a state object, the content of which has defined the UI elements. User interaction (or just the order in which the elements are rendered in the browser) then triggers actions, which are sent to the pure reducer functions, which finally update the store instance by creating a new state object containing the updated elements. To instantiate Redux as a state management system, we need to provide an initial instance of the state object that will be used by Redux.
In this example, datasource and data properties represent different resources that need to be merged into the enrichedData property. Using the initial state object, we can configure our Redux store and then pass it into the Provider component, which enables the usage of that store in all child components, beginning (in this instance) with the App component
The store requires middleware that enhances the features this store instance has. Usually, in our case, we include sagaMiddleware, based on the Redux saga library. Saga is included in the Redux lifecycle as a step between actions and reducers and offers various operators built specifically to allow an alternate side effect handling. Just from this example, we can already see the Redux boilerplate. To configure the store, we need to provide a reducer, a pure function which will, based on different triggers, create new instances of the store state, and in that way propagate the change to all components using the state.
The reducer function takes the initialState in its first appearance, and an action object which consists of a type (discriminator between different action types) and payload (data passed into reducer function). Finally, we develop a Redux Saga functionality as a middleware for different asynchronous behaviors in our application.
In order for our components to have access to the Store state, we would use React-Redux’s connect HOC, which accepts the mapState and mapDispatch pure functions, and creates a new function which then accepts our target component and merges the props, thus providing us with a component able to utilize the Redux features.
Why we decided to stop using Redux
Up until recently, the frontend for our platform was based on React 15. Although we have migrated to React 16, the stack of the entire application is so tightly coupled (due to the years of organic growth), that it became apparent that instant utilization of newish React go-to’s in the legacy code, like hooks, is not possible. We decided to go with the overhaul of the application, and thus, as one of the largest portions of its codebase, of the state management as well. We wanted to start using something a lot less boilerplate-y, something that offers more operators and more control, as well as something that is not considered an anti-pattern, as HOC’s today are.
What is RxJS
RxJS’s Subject is a special type of observable that different observers can subscribe to. BehaviourSubject, however, offers more features than a regular Subject – it always returns a value, even if no data has been emitted from its stream yet. In order to emit an action, we can create an intent stream, an instance of a Subject, whose whole purpose is to trigger async events and, once they are completed, write the data to the data$ BehaviorSubject.
The result of some of these streams can easily be merged together to create a new observable.
This newly created stream structure can be observed in the functional components by utilizing a few hooks – React’s useEffect hook, and react-use’s useObservable hook.
UseEffect is used to provide side-effect behavior that can be triggered on change of any of the variables passed in the dependency array. In this example, the dependency array is empty, thus ensuring that the side effect inside the useEffect’s callback is triggered exactly once. Next, we use the operator on the intent stream, which emits an action that triggers, for example, the process of fetching and transforming the data consumed from a web resource. The useObservable hook observes the merged$, a merged stream that, in this example, combines the latest values of different data streams, and provides that data inside the functional components that are using it. If you want to read more about what RxJS (or its operators) is, and how it can be used, you can check out this link: https://github.com/ReactiveX/rxjs.
In short, RxJS offers a lot less boilerplate in order to set up the application, and the organic growth is easier to maintain. A single global setup of Redux-based state management system would look like this:
│ │ App.tsx
│ │ actions.ts
│ │ reducer.ts
│ │ saga.ts
│ │ store.ts
In addition, any module-specific Redux functionality can be implemented deeper in our folder structure. However, regardless of the number of actions, reducers or sagas we need, it is expected that their implementation is separated similarly to the above-mentioned file structure. Of course, if we implement common sense, all of the Redux functionalities can be split into different files, grouped by their business logic.
In the RxJS example, however, basic folder structure might look like this:
│ │ App.tsx
│ │ streams.ts
Instantiating different global or module-specific streams provides a similar (if not the same) functionality as the Redux stack did, but it does that in a lot less code, ensuring that setup, development, testing, and finally debugging processes are quite less time-consuming.
So far, our experience has shown that RxJS is indeed a lot easier to maintain than Redux code. Managing global streams, specific module streams, merging them together and thus creating new observables for the specific use cases, creates a lot less confusion and leaves the frontend technology stack a lot cleaner.
Continue exploring the Dev Corner to meet our dev team and read a tech blog or two
Check out the t-matix main website if you are interested in the t-matix group and want to find out more about our solution as well as industries we cater
December 18, 2019
Continuosly building, testing, releasing and monitoring t-matix mobile apps