React JS: Inherited project maintenance journey

From a front-end developer's point of view this is what happens when a team inherits a project and their task is to work with updated designs. With react component inheritance the challenges that we faced and the positives and negatives of the complete journey is explained in this blog.

First, the team analyzed the existing software & found some “issues”. Issues like: using class components (more about this later); huge file size (1000+ lines) with no clear organization; using old library version; Clunky Routing; Lots of Unused code / Unreachable code / Unused files; Huge comments with no clear purpose; Logs without purpose; Unsafe methods; etc.

It did have its good sides too. The core architecture was really clean and it used good practices, to begin with. Over time, one can read the fall from their own highly set standard. Or maybe I’m reading too much into it.

React version update

While using class components in itself is not really a bad decision or bad practice, like it's so complicated, and functional components can make it so much easier. Lines will be reduced and you don’t worry about the this keyword. Our task was to implement existing functionality in new designs so it meant that we had to write most of the modules from scratch. So we took that as an opportunity to use functional components.

The first thing the team did was to upgrade to react version 16.4+ so that we could use hooks and functional components. At the time of writing this post, the latest release of react is v18.0.4. In the interest of not breaking existing code for deprecation reasons and library dependency, we decided to use the most recent version which meets our needs. In the near future, a bolder migration is in the plan.

Other library updates

We analyzed the core libraries that the website depended on, and tried to upgrade the libraries.

UI analysis

So the team had just received the new designs and the fact that most modules shared a very consistent design was, of course, a given (thanks design team) from this, we quickly noted down the common designs and saw big opportunities to reuse code. Things like the header on each page; The tables on the site; The forms were very similar; The detailed view of any modules followed the same card designs; The filters were everywhere and used the same design.

Actually working on the new designs

Then came the time to work on the new designs finally. Developers were assigned their own modules to work on.

Some common ways to speed up development which we eventually found are discussed below.

So for the pages with very similar layouts, we created CLC - container layout components and used that on all the pages. This may sound simple but in practice, it took at least 3 to 4 iterations before developers were happy with the container and they started to use it in their modules. This holds true for most CLC that we created. Of course, some were faster to be adopted simply because of the sheer simplicity and frequency of requirement, for example, the header component while having many props and many features were fast to be adopted because nearly every page used it and it had a simple clean api.

The anatomy of a container layout goes as such.

We now have container layout components for nearly everything, and this speeds up development time tremendously.

Here are some container layout components that we have created over time:

HeaderWithBackButton, PageLayout, Search, Table, InfoHeader

If you can guess the purpose of these Components from their name, the team has won. But even I can tell we need work.

CLC serves as a base for all the abstractions we do for our application…

While Developing

CLC - container layout components ****are not new and everyone uses them but It's not so easy to get them right and keeping them maintainable gets progressively challenging with more feature requirements. The key to keeping it a good component is to keep it simple. If the new feature request is seemingly a part of the CLC but implementing it involves yet another conditional and another prop just for the sake of the new feature, then maybe it's better to create another component for that instead of taking away from simplicity for the sake of compactness.

Then again when is it acceptable to extend a CLC ? maybe when yet another conditional and another prop just for the sake of the new feature is not required? let me know too. Sometimes compactness does not pay off.

CLC was useful but we frequently saw that depending on which module it is used in, i.e; its context, it had similar props all the time and so we saw that it could be abstracted out with those props passed so that it can be called with fewer props and more logic can be abstracted in a localized place. This is an abstraction over abstraction.

Here is some sample pseudo code illustrating the situation.


In practice, AbstractedContainer will do initialization and much boilerplate stuff.

Below is a slightly simplified real-world AbstractedContainer example,

It fetches some data from an api and provides the layout, its only dependency from its parent is handleSubmit

import { message, Modal, Select, Spin } from "antd";
import React, { useEffect, useState } from "react";
import { DEFAULT_CITY } from "../../constants/Common";
import callApi from "../../services/networkUtils/networkUtils.index";
import FilterPin from "../ESCommonFilterPin";
const { Option } = Select;
/*
Location filter is AbstractedContainer which only requires the handleSubmit
*/


export default LocationFilterModal;

This allows other developers to focus on other problems. With this approach, developers can spend their time, solving problems not yet solved and not re-solving something already solved.

Here is an excellent article on the nature of abstraction.

Not a smooth ride.

I know you are thinking that it probably wasn’t so simple to adopt this in the project, and you would be right. Situations like developer A is developing CLC-A and developer B wants to use CLC-A in their module even before CLC-A development is not complete. This can happen because of conflicting timelines and a lack of planning before development.

CLC-A is already developed but dev has no intention of using CLC-A simply because they are not aware of it so they build one themselves. After building, there is now two component which is doing the same thing, and it's not an easy thing to tell someone to drop their work. This can happen due to a lack of communication at the right time.

CLC is developed but is used in the “wrong” way. This is a classic and this can be for various reasons like

  • Not being able to follow along with integrated work due to differences in class vs functional components.
  • Not understanding the use case of CLC
  • CLC with hard API is often misunderstood and setting it up can be challenging especially for other developers who are not involved in the development of CLC (this is not a good reason because a CLC should be easy to use and set up).

I am sure there were other non elegant juxtapositions! but I can’t remember them. If anything I think the CLC abstraction is really neat and can save a lot of time. Section while developing is actually really fun and interesting, so don’t miss it.

Conclusion

At Fibonalabs we have developed several projects using CLC. It has helped our developers in saving a lot of time and coming up with clean, less complex modules that can be of great help even for the projects with react component inheritance. Our developers ensure that when it comes to maintenance they put their best foot forward and use the best of their skills that do the job in less possible time. Along with our excellent product development services, we provide UX design and cloud services and have helped several clients in reaching their goals. You can also visit us at www.fibonalabs.com and avail the best of our services.