How we moved from ng-admin to React & Redux

Lod LAWSON-BETUM
NI Tech Blog
Published in
9 min readMar 31, 2019

In this two-part article, I will talk about our journey while building a new back office system for our CMS. I will discuss the challenges we faced, the mistakes and the improvements we made.

  • In this part, we focus more on our experience with using React for building the App.
  • The second part will cover our methods of testing the App.

Our goal was to design a new Content Management System from scratch to manage multiple sites and their content. A Microservices Architecture was our strategy. Each service in the system hosted a group of related entities. We successfully managed to define infra packages CRUD, API, etc. Our services were ready. Now we needed to build the client side to consume and update data stored via those services. Obviously, it is best not to reinvent the wheel, when there are libraries out there that have been built and tested by many contributors. So that’s exactly what we did.

One of our Tech leads found an Angular 1 based Admin Library called ng-admin. A POC was done and wowed product managers. We quickly began building the back office. Each developer was assigned a set of screens per entity. We were making really fast progress and soon many screens were ready and content managers started using the Back Office. However, as the demands from product managers became more and more complex, it took longer to build new features as ng-admin was lacking some of the needed features out of the box. Adding new features required a lot of code to customize the new back office screens and required ugly hacks. Most of the time, adding a simple feature just seemed to require a lot of work.

Many of us agreed that this was a painful process and needed a more efficient way to proceed with new features. We needed more control, faster development cycle, a leaner codebase. We decided to rewrite our back office in React, all excited to leave the mess that we had created. The product managers were told that ng-admin was limiting our capabilities and we needed a new system if we wanted to work faster. It would be our own codebase, we will have more control and make changes faster. They agreed and we were given a month to migrate our Back Office to React. We had a happy team motivated to build a shiny new system in React. Aside a newly hired architect none of the developers knew React. It was an interesting challenge and I guess it is something that we love at Natural Intelligence. Who does not like a fun challenge?

Fast forward to a month after, we did not manage to deliver a complete system. Not all screens were covered. No tests were written at that point. We had a lot of technical debt in our backlog. However, we all agreed that it was the right choice. We learned a lot in the process. The following months we focused on making more screen migration and writing tests. Many more lessons were learned in the process.

The Challenge of Microservices

The first issue we faced is not related to React. It was more of a Microservice dilemma. One of the disadvantages of Microservices architecture is the fact that entities are not stored in the same database. So no SQL JOIN or MongoDB aggregation lookup here. For example one of the screens needed a list of related entities. In monolith service we would just inner join in the database and voila. We thought of GraphQL at the time but it was too ambitious since we only had a month to migrate to the new system. Not only did we have to quickly learn to build a React App, together with best practice, but we were also now faced with another new technology to use. Using GraphQL meant that we have to design schemas, resolvers, worry about caching, investigating long queries, pagination, etc. We decided to do without GraphQL. Our solution was to join our entities on the client side. We managed to find a short implementation that was not the most elegant but effective.

Another challenge we faced was actions that required the creation and update of multiple related entities in different services. This is another problem related to Microservices Architecture — no transactions. For example, when a Product is created, a Link was created, the product was added to a Product List and a Rule is created to automatically hide it in the product list. What happens if one of the stages fails, do we rollback, do we do an optimistic update and retry later?

There are many possible solutions that we considered, but have not implemented any yet:

  • API Gateway with GraphQL. GraphQL queries would be translated into API calls to services.
  • API Gateway with an orchestration of actions. A single Service knows all the other services and knows additional actions to take when a specific action occurs.
  • Publish-Subscribe messaging pattern.
  • … And many more.

I am sad to say that due to a lack of time, we implemented all this complex logic on the client side. But are now working on moving this logic to the server.

Inheritance vs Composition

A quote from reactjs.org on the subject.

At Facebook, we use React in thousands of components, and we haven’t found any use cases where we would recommend creating component inheritance hierarchies.

Props and composition give you all the flexibility you need to customize a component’s look and behavior in an explicit and safe way. Remember that components may accept arbitrary props, including primitive values, React elements, or functions.

If you want to reuse non-UI functionality between components, we suggest extracting it into a separate JavaScript module. The components may import it and use that function, object, or a class, without extending it.

Many introduction articles to React mention that composition is a better way than inheritance when reusing a given behavior. However, OOP is always tempting when you want to reuse code. And some of us sometimes succumbed to that temptation.

The problem was not composition vs inheritance. It was deciding were each was relevant. For instance, we used composition a lot for presentation components. However, when facing Containers, many of them had very similar non-UI behaviors. Many components had very similar behaviors related to React component life cycle such as

  • Setting default values.
  • Cleanup state values (errors messages, data, etc.)

In those cases, a base class had behaviors that children component inherited. Inheritance was also used for sharing common event handlers.

As in any design pattern, there were some issues with using inheritance. For instance, since we used class fields for event handlers we were faced with some of the issues listed below.

  • Mocking: Cannot mock methods on the components class prototype since class properties are transpiled to instance methods by Babel. For example, the code below
class MyComponent extends React.Component {
onClick = (event, data) => {
//...
}
}

is transpiled to

class MyComponent extends React.Component {
constructor() {
this.onClick = (event, data) => {
//...
}
}
}
  • Overriding handlers: No option to override inherited event handlers. For example, the code in the snippet below will not work
class ChildComponent extends MyComponent {onClick = (event, data) => {
super.onClick();
//...
}
}

A quick solution would be to define event handlers in a different module.

// handlers
export onClick = (event, data) => {
//...
}// class definition
import handlers from 'handlers';
class ComponentA extends React.Component { onClick(event, data) {
handlers.onClick(event, data);
}
}
// class definition with custom onClick
import handlers from 'handlers';
class ComponentB extends React.Component { onClick(event, data) {
handlers.onClick(event, data);
//...
}
}

Moving handlers to a separate module provides more flexibility.

  • Handlers can be mocked easily.
  • Event handlers can be used as is or with additional behavior (as shown in ComponentB in the last snippet).
  • Event handlers can be tested separately. This is much better than testing event handlers in a Base Class. When event handlers are defined in a Base Component, testing them requires component instantiation (via new or rendering).

Improvement

We constantly refactor our code. Moving to composition is something we are doing as part of this process. It is still a challenge since most of our time is allocated to building new features. The good news is that developers vehemently complain about painful areas of our code during Retro and Code Review meetings.

Redux

Redux is a very good data management library for React. However, a single store means many components can have shared access to the store.

Single store

Common issues we faced

  • Data corruption by a component in the previous screen. This commonly due to a component not cleaning up the state when destroyed.
  • Some components were originally designed to have a single instance per page. When multiple instances of those components are needed on the same page then the store needs to be restructured to avoid conflicts.
  • Not really a Redux related issue, multiple API calls to the same resources by different components on the same screen. This is done to allow isolation of components’ data in the state. This is a problem that we are fine with.
    A solution to this is memorizing API calls. However, this might require a finetuned configuration based on the type of data, a frequency of changes. It also requires a mechanism or triggers for invalidating cached data.

Reducers

For improved performance, Redux uses a shallow equality check (check by reference) while listening to state changes. This means that reducers must be pure functions without side effects. In other words, Redux expects reducer functions to return a new state object when changes to the previous state are required, otherwise, the old state must be returned. There are libraries that can be used to reinforce immutability. However, we did not use any. There were cases where reducer functions updated the old state and returned it. This breaks the rule of function without side effects and caused components not to update in the UI, even though the state was updated. It is not very common, however, we all agree that it needs to be solved. Not using an immutability library was a decision related to the lack of time. Enforcing immutability is still a consideration, however, our codebase now is very large and would require a major refactor to reach this goal.

Another rare problem we faced was the update of deeply nested fields. This led to code that was verbose and difficult to read.

function myReducer(state, action) {
return {
...state,
firstLevel: {
...stateFirstLevel,
secondLevel: {
...state.firstLevel.secondLevel,
secondLevelField: secondLevelChangedField
} } }}

Many libraries such as object-path-immutable and dot-prop-immutable provide helpers.

import dotProp from 'dot-prop-immutable'
function myReducer(state, action) {
return dotProp.set(state, 'firstLevel.secondLevel.secondLevelField', secondLevelChangedField)
}

Another good practice is to keep a flat state and use reducer composition where possible.

Containers and Routing

The first containers components added to the app were aware of the route and had routing capabilities.

The snippets above has two components responsible for the creation and editing of an entity. The problem with this implementation is that these components were not reusable since they are coupled with the routing system.

The implementation in the snippet above is a much better solution than the previous. Components are no longer coupled with the routing mechanism and can be reused in multiples screens and in contexts that are not related to the route.

Changing UI Library

When we started with React, we used Semantic UI as our UI library. Our components depended heavily on Semantic UI components. One of our team leaders mentioned the fact that we need to wrap Semantic UI components with our own. However, time was something we did not have. Our tests also used events and attributes of Semantic UI components. In short, our codebase was coupled with Semantic UI.

Another challenge we faced was when migrating to a new UI Library. We wanted Material UI instead of Semantic UI. The migration to Material UI was a very painful process. Many of our components needed to change since the APIs of Semantic UI and Material UI components are different. Could we have avoided this? At this moment, I don’t think so. You are always coupled with the framework you use.

Almost all our tests need to be updated since they were coupled with Semantic UI. Luckily some engineer created helpers, so the migration of some of the tests only required changing test helpers logic.

Conclusion

There is nothing special about what happened in our team. It is a cycle in development, we design, make mistakes, review and improve. The cycle goes on. What is important is the lesson we learn. Tackling this project again, we would have done things differently.

We still did not solve all our problems; we have alternative solutions that we are still considering. I’d be glad to hear your comments and solutions you had for similar problems. In a future article, hopefully, I will mention the solution to the problems that are still not solved.

Thanks for reading.

What’s Next?

What is code work without testing? This article was initially supposed to include testing. However, I realized that it was a topic that deserves its own article. In my next article, I will cover the types of test that we use.

  • End to end tests: Involved running our App and services it depends on in Docker containers before testing.
  • System Tests: Testing of Containers with Enzyme and JSDOM. Tested components are connected to the Redux store. The API calls are mocked.
  • Unit tests: Testing of Presentation components.

--

--

Lod LAWSON-BETUM
NI Tech Blog

In short, I am a Full Stack Developer that is learning every day and is enjoying every moment of it.