One of the biggest challenges in software development is dependency management and software architecture tries to manage it. Different architectural styles try to handle dependency management in different ways. Microservices are the newest trend, where one microservice is trying to be as autonomous as possible and therefore has no coupling to other microservices.
The main benefit of having loose coupling and minimal dependencies is making it easy to change the system without fear of breaking something else.
When talking about coupling and dependencies, we are usually referring to backend code. However, the same principles apply to the frontend code as well. One of the most discussed frontend topics in the field is state management as it’s present in all frontend applications, especially single-page ones.
There are many blog posts, that compare different frontend state management solutions like redux or mobX. However, there are not so many blog posts that touch on fundamental problems that a shared state could lead to.
In this blog post, we will investigate how sharing state across the solution may lead to hidden dependencies, which will have a huge impact on system maintainability.
About the hidden dependencies; they are one of the nastiest dependencies. They are created when multiple features refer to the same data structure on external storage (https://martinfowler.com/bliki/IntegrationDatabase.html).
Hidden dependencies in the backend
let’s have a quick look at the hidden dependencies on the backend side. Let’s have an example using three features A, B, and C:
- Feature A and C queries data from table T1.
- Feature B is changing the data on table T1.
- Feature A doesn’t call any methods in Feature B or C
- Feature B doesn’t call any methods on feature A or C
- Feature C doesn’t call any methods in Feature A or B.
Now we have a situation where we do not have any visible dependencies between Features A, B, and C (which even a static code analyzer cannot see). However, all those features are depended on data structures in Table T1. So, all those features are depended on each other.
Now if we’ll get a new requirement that forces us to change feature A’s data structure, features B or C may get broken because of this change. We also have the same problem in the frontend when our components are connected to a shared state (redux, mobX, or something else).
And back to frontend state handling
State management solutions are libraries like redux, mobX, and overmind. I won’t go through deeply why we want to use state management solutions. In general, we use those to share some data between components.
Many people argue that mobX is a lot better than redux. Usually, because it is simpler and doesn’t have so much boilerplate code. Because of this kind of discussion and comparison between the state management libraries, we usually miss the point; boilerplate code is not the real problem, dependencies are.
At the beginning of the project, you won’t even notice dependencies. With loose dependency management, you can develop the system faster. However, when time pass and the system grows, you start feeling pain. The growing spider web of feature dependencies will slow down the development pace due to added complexity and makes your system more fragile.
When we share data across components, we may quite easily end up in the same situation we described in the backend example.
How to handle state in the frontend
You have two options:
- Use a global state management library: If you go with this path, remember to isolate your data areas. Isolating means that you should ensure one state slice is used by one feature only (this is of course just a rule of thumb and there are times when you want to break it). If you have an isolated slice of data just for your feature, you are doing the same thing that microservices are doing in the backend.
- The second option is just to use a react component internal state (useState or useReducer hooks). This should be your first choice because it is a “KISS” (Keep It Simple Dude). Then you have to do prop drilling and maybe use Context API in some cases. However, that is a small price of having a more maintainable system. A warning about Context API; you can end up same problems as in option one if you start using Context API as a global state.
So why am I raising the issue on the frontend side? In the backend, we usually have clearer boundaries between the features. That makes dependency management more explicit and easier.
In the frontend, this is usually harder, because our feature boundaries are more blurry. We have a lot of features on one page and they usually show the same or similar data. That leads very easily to a bad way of data sharing, which will cause poor maintainability.
So, as a conclusion, it is actually fine to use state management libraries. The challenge in using these libraries is the easiness to access some other feature’s state. By doing so, you will create a hidden dependency.
Usually, you don’t need those state management libraries at all and that makes your system’s architecture simpler. Simpler systems are always more maintainable, so think twice before adding this extra complexity to the system.
For the record: During the last two years I have coded using React I haven’t needed any state library. Having the react components handle their own states is usually enough. Also have a look at a library like react-query, that helps you to handle some use cases where you usually need a state management library.