In React, create abstractions also around data to increase reusability of parts in your code.
Defining data structures within your code can be hard. Especially when thinking about your application as a bunch of simple strings and numbers just being printed onto the screen. Even more so, when your toolkit of abstractions consists only of Components (including hooks) like it does in React.
In this article, we will look at a process of creating abstractions around data rather than UI, and see which data abstractions can bring a lot of value for building reusable/composable parts in code.
💡 Code examples on purpose omit styling. You can check out the whole working example on Github (soon)
At first, let's look at an example of a simple cryptocurrency exchange application. At the top, we see a summary of a user's wallet. Underneath it, there is a list of tickers showing top trading instruments, and below, a list of assets the user's wallet consists of.
Moving right to the implementation of it we'll start by looking at the 'PriceWithChange'.
The initial component interface for it is pretty straightforward.
Components responsibility is to format values and style them accordingly.
You can compose those components in your application by passing them as children or adding some display logic in 'Price' and 'PriceChange' i.e. in our application having the values aligned next to each other in the header and tickers or one under another in the rows. Whatever you feel works best.
Finding oversimplified contracts
Let's say now our application needs to have a new feature added that changes the display of the percentage 'PriceChange' to an absolute amount when pressed. With our current solution adding this would look like this:
What had to be added is component state storing formatting that is currently displayed (here using an 'enum' toggle). Moreover, we need to expand this component's interface by 'absoluteChange' used when showing the absolute amount.
The parent components interface also has to be expanded because of that.
For better reusability, we can extract formatting logic to helpers to further avoid duplication.
And use it as such:
But what is actually shown on the screen?
Looking at what we are trying to display for the user rather than how it looks we can definitely see that we are not just showing a random value formatted as percents in 'PriceLabel'. In fact, one can argue that we are trying to build a generic component doing a very specific task. Because it does not only show our number but also makes assumptions about it.
In fact, 'PriceLabel' component assumes that the value it receives is not only a number but a number that makes sense being shown in USD. Similarly 'PriceChangeLabel' expects the same number and one more that can be shown in percent. When you see this type of assumption done in UI components, it usually means that the data related abstraction is missing in code.
In examples stateless classes are used. You can replace them with any preferred form of gathering data in one structure i.e. bare objects with getters / methods.
Let's try to think about it as a more complex structure than a simple number then. The first thing that comes to mind is a 'Price' which can be formatted as a string.
We can then pass this 'Price' structure to our component instead of number.
This simplifies this component greatly and gives us the possibility to reuse not only this 'PriceLabel' in different places but also the data structure itself. It also replaces the generic but not really helpers.
And the beautiful thing about this abstraction is that we can also build on top of it! Thinking a bit more about 'PriceChange' as a price change we can notice it is in fact a delta between two Price's (which we already defined in our system), that can be formatted as percent string or amount string.
Now also PriceChangeLabel's interface makes much more sense than previously.
After defining our data structures using them in components becomes much simpler and logical. Those structures tell what has been previously assumed in every place in the app that was showing this type of data.
'PriceWithChange' now receives a 'PriceChange' obviously. 'PriceLabel' receives a 'Price' that it knows can be formatted similarly to 'PriceChangeLabel' which knows that 'PriceChange' can be formatted in two ways.
Data abstractions change your mind
Wherever in the application we now want to show a change of prices we would use the exact same interfaces. i.e. in the tickers showing instruments.
Or at the top inside of the wallet summary:
By creating the idea of a 'PriceChange', a structure encapsulating not only data but also logic related to it we drastically changed perception on how to look at the application as a whole. Now we think about not only green / red box displaying a number formatted as percents but green / red box displaying the change in price. This makes much more sense on both: code and business levels.
What we learned
To sum up, building generic UI components (or what I call UI abstractions) that can be reused across our application makes a lot of sense, but usually does not bring much value. Moreover, it can make more harm than good unless you are working with a precise and well-prepared design system because we're building reusable components with a lot of options for changing.
On the other hand, building well-defined reusable data structures with encapsulated logic in our system, even in frameworks like React, gives us a lot better understanding of what is in fact shown in our application, bringing a lot of value on its own.