Clean Code UI Refactoring at Axon 🎉
In this post, I would like to share my experience on how I did refactor fairly messy React's context based codebase into clean, nicely encapsulated abstractions backed by SOLID principles and application of several well-known design patterns. Stay tuned and read on guys 😎
At Axon, we provide a solution for defense departments around the world to view different kinds of evidence in the web namely audio, video, image, document, log. The latest exciting of supported file format is zip (compressed evidence). I have been part of UI code maintainer group at Axon's DEMS (Digital Evidence Management System), we are responsible for assuring high code quality review before shipping our products to production. I got a chance to review with the team that was developing Zip viewer for the last several months. The bellow is their stack choices of technology:
- React's Context - be careful application developers, it's mostly useful for libraries 🤭
- React's Hook
- Redux (it's just a fancy React's Context underneath the hood 😆)
First, I have never been a fan of Flux / Redux architecture given its terrible verboseness of boilerplate we must write. Redux toolkit came into the picture lately to heal things up, but it's still Redux at very core in which suffer from awkward choice of merging local state and server state into one. Things will get even worse when you want to cache something or integrate with WebSocket... That's where React Query really excels at. It's my go-to recommendation for any application that deals with data fetching Restful protocol. But you most likely have maintained at least one application that has used Redux in recent years since it's been blooming back in 2015 via this talk. I'm right with you, many projects at Axon are also suffering from the same issues with Redux's drawbacks and lack of principles from the very beginning until style guide appeared apparently. Another issue with Redux lays in its implementation of O(n) time complexity in every run by default. It's not scoped by nature and high performance in comparison to Vuex. Solutions like Redux-ignore does help a bit but still... I think performance should be built-in instead of letting community brainstorming which leads to exploding number of solutions.
Secondly, the team chose to use React's context for everything at the top level which they thought it could help them avoid prop-drilling. This comes at the const of breaking encapsulation and potential for code reuse in other places. It also leads to many problems specifically:
- Context Provider easily became a god object which knows every little detail of all child components
- Simple state such as toggle show / hide modal is also included in the context which has nothing to do with other logic
- There is zero chance of code-reuse and composition since every child components stuck with consuming specific context
I started it by convincing the team that React's context and Redux are not the right tools for the job with the reference to React's official guide. After that, I kicked things off by moving all pieces to where it belongs by purely using local state. It's actually very similar to actor model for UI development 😄
We also love embracing the separation of concerns aka The Clean Architecture, someone might still remember it as Container/Presentational Pattern which was really famous back in the day. I still think it's really important to have it under our toolbelt while developing your application. Container components take care of how things work, presentational components take care of how things look. Because are are building software that is optimized for change and future proof, not just works for today. It should be easy to change as per requirements! Given clear layers among components, we can swap things along the way as we see it fits - just be pragmatic! 😊
Source - The clean code blog
You also might want to spend some time to enjoy the brilliant content of this video 😎
I also explained several principles applied along the ways to the team such as SOLID in component architecture, Lifting State Up actually is Mediator pattern in disguise and should be used more often, Factory Method pattern is also very useful for grouping a set of similar components at the point of creation (e.g. Icon set), Strategy pattern plays a key role in selecting the right evidence viewer for us (there can be any video, image, document, etc. inside a zip evidence), backtracking algorithm is a very effective way for us to collect all the paths that contain items matched arbitrary predicate in a tree data structure (i.e. zip file). The demo below shows how codebase looks before and after my big shot of refactoring:
As a result, we now have variously dedicated components (actors) which are in charge of specific tasks in our component architecture. We can easily let them communicate by talking to each other (trust me, prop-drilling is no biggie at all), reuse elsewhere and tested in isolation.
Later this year, I also learned that UI composition with JSX as children is a much better approach than React's Context plus avoid common props drilling problem - top React skill to learn in 2023 by Dan Abramov from React team
There you have it guys, my story on how I untangled messy codebase into clean, encapsulated, nicely formed abstractions for maximizing code reuse and maintainability. With that, I hope you guys learn something useful, avoid the same mistakes we made and enjoy problems solving in this world 😇