Simplifying UI state management
How moving to a redux-like structure made things so much easier
The background
Over the last few months the Orca team at Redgate have been building a migrations-based solution for database DevOps focusing specifically on Oracle called Redgate Change Control. This has entailed building an Electron desktop application using React that communicates via some simple APIs to the Oracle comparison engine that powers the tools in our Deployment Suite for Oracle, and to Flyway.
The UI is superficially quite simple; it consists of a landing page for managing your projects and two further pages that allow users to generate database migration scripts based on changes made in a development database, and apply pending migration scripts to the database. We started from a blank page and built up the necessary functionality until within 2/3 months we had a functional product that we were able to release to EAP customers to start getting feedback on.
Whilst reaction was very positive, both from within Redgate and from our EAP customers, in terms of what the tool could do to ease the pain of creating and managing migration scripts, one significant area for complaint was performance. Specifically, generating the list of changes made in the development database from which migration scripts would be generated was slow. Really slow. At least it was for Oracle. Interestingly, in order to future-proof our design against the likelihood of incorporating additional database types in the future, we had built some basic functionality for MySQL using Redgate’s MySQL Comparison engine in parallel and we found the comparison was way faster.
Make it faster. There is no try.
With a target of both getting our EAP customers to use our tool in anger in production environments and an aim of releasing a public beta a few weeks later, we knew we had to take a serious look at improving performance. This led to some thinking and some exploratory work looking at two areas: making the comparison process faster, and making it appear faster. The first we tackled by profiling some of the Oracle SQL queries and making tweaks to how they ran. However most of the gains looked like they could be made in the second area.
We started by making a number of UI tweaks, such as replacing spinnys with progress bars. Then we started thinking about caching data and doing background refreshes. At which point things started to go weird. Because our app had been so simple, with only a few, isolated React components, most of our API calls and state management was being done at a component level. This was fine as it was easy to reason about and maintain — however as we started to implement things like background refreshing, we found that the components became much more inter-twined and state started leaking between them.
Inevitably this started causing bugs. Due to the nature of the application we were building, error messages were particularly important as they fell into two 2 areas. Application errors (i.e. where we had written some bad code) and “project” errors — legitimate messages from Flyway explaining that something about the migrations in the project was in a bad state (e.g. missing from the project but applied to the database). We needed to handle these differently and present users with different options. Unfortunately as the application state got more tangled, so we found that these messages were displaying on the wrong pages or at the wrong times. Often an error message just wouldn’t disappear even after it had been fixed until the app was restarted.
Confusion reigns
Of course not only were we getting lots of bugs, we were finding it really hard to work out how to fix them. In most cases, we would make a change that resolved one issue, only to cause another one to pop up. We no longer had a clear picture of how our app worked or how our components functioned together. We had also had to introduce extra components purely for the purpose of maintaining state and passing it around increasing overall complexity. This became even more apparent when our team gained new developers — we struggled to articulate how the code worked and it was difficult for them to get up to speed.
As a team therefore, we took the decision to stop trying to fix the bugs and instead tackle the root cause. We knew we needed to simplify and streamline the management of UI state and we wanted to decouple our presentational components as much as possible. In essence we wanted to implement a solution that looked a lot like Redux. We weren’t that keen on jumping straight into implementing a full Redux solution however — we use typescript so already get a lot of type-safety for free, our code was quite difficult to disentangle and the additional boilerplate etc felt unnecessary given that we were already using React Hooks.
Instead we decided to take a staged approach of pulling state upwards out of individual components into a couple of large reducers at the top level, and building action creators for dispatching calls to the APIs. The whole team got involved in a series of mob programming sessions and we dedicate most of a sprint to tackling this. Eventually, we started splitting up our reducers and converted our action creators into custom hooks which made it really easy for our components to get the state they needed as well as trigger any actions that were required.
Tackle the cause not the effect
Whilst this work is not yet complete, we have made great progress and will keep driving in this direction as we continue to develop the app. Making this change solved a number of our bugs as we went and everyone on the team now understands the architecture of the app. Over the last week or so we have started tackling the remaining bugs and found the code so much easier to reason about that fixing the issues was relatively painless.
At some point we may end up moving to a full Redux solution but that doesn’t feel like it is necessary at the moment. Instead we are working on consolidating and simplifying our reducers so that everything works together in a more seamless manner. It did take a fair bit of team-time to get to this point, but the benefits we are now realising make it totally worth while. Redgate Change Control is now more responsive, more consistent in how messages are displayed and handled and the code-base is much easier to work with.
Moving in this direction should greatly increase the speed with which we can develop the app as we continue to extend its features after its release as a beta. It can be a daunting prospect to significantly rewrite a major part of your application, but done right, the benefits can be both immediate and persist for a long time to come.