Isolated components driven development is a technique I've been using for years during development or debugging, which proved to be incredibly useful and even necessary in some particular scenarios which we'll cover in this article.
It is not a novel technique. We're used to developing code in isolation in unit testing, by executing a function or method outside the application that uses it.
Component -> f(props)
If we think of a UI Component as a "function of props", we could also render components in isolation, outside the application that uses them.
There are multiple scenarios in which this technique proves helpful. Let's explore different use cases which I have personally experienced in the past.Building a UI components library
A UI components library is a collection of reusable components used in one or multiple applications. Such libraries are usually developed separately from the application that uses them. Therefore, we need an environment to render the UI components during development.
So, the technique is absolutely required in this situation because we're implicitly developing the components in isolation.Working with read-only UIs
Typically, most User Interfaces include all CRUD operations. This allows us to
Create data, which is needed to build UI components during development. Without the data, we could only render the component's empty states.
However, some UIs, like Google Analytics charts or Github diff view, are primarily read-only. They read and display existing data, but we cannot add new data from the UI because the data source is different from the destination that displays it.
Even if we have access to the data source in development, adding test data or having an integrated development environment might be cumbersome to use.
A more convenient solution would be to develop the UI components in isolation, bypassing any data sources and rendering them effortlessly with any arbitrary props and in any desired state.Working with unstable APIs
There are multiple methods to connect the UI to their web API, each having some strong points and limitations. But sometimes, the APIs that we integrate with might be unstable for various reasons:
- they might be under development, changing frequently;
- or they could be offline when we need them most.
Decoupling the UI components from their data source could significantly improve the development experience because it removes the dependency on unstable APIs.Working with deeply nested data-dependent components
Let's consider the most favorable scenario. We don't have to deal with a read-only user interface. We're not building a components library, and we're working with a very stable API.
Working on a deeply nested component might require a few steps to get the application in the desired state:
- boot up the entire application;
- navigate to the required route;
- fetch the data from the API;
- pass down the data to descendent components and trigger some events;
- fetch more data, potentially from 3rd party APIs;
- and eventually, display the new component that we should be working on.
That's an immense amount of overhead only to get our application in the required state. Not to mention that we might have to debug that component using a specific application state that might be cumbersome or difficult to replicate.
Decoupling UI components from the entire application and data provider containers would significantly improve the development of new UI components and provide a smooth experience when debugging existing ones.
To begin with, we don't necessarily need special tooling for this technique. For simple applications, we can use separate page(s) to render the required components. For example, I hadn't built a single page of this website before all the required building blocks were implemented and rendered in an isolated style guide page.
But for larger applications we would probably need more advanced features, and there are several existing solutions that we can choose from. I've personally used two different libraries in the past with great success:
- React Styleguidist is specifically built for React. It's pretty simple and basic, so it's a good choice as a starting point for React applications.
Both libraries provide high-quality documentation and examples, so I won't dive into the details. Instead, the focus of this article is the general approach of isolated components development, not on the technicalities of implementation.
As we saw, isolating components is beneficial during development and debugging. But there are additional valuable benefits that are worth mentioning.Documentation
Having all the component states rendered in a separate UI provides excellent documentation for anyone working on the project, allowing them to quickly discover the existing components. It also helps avoid reinventing the wheel and creating similar components, prevalent in lengthy projects with larger teams.
Both previously mentioned tools automatically generate a list of available props based on the type definitions of the components.Interactive playground
Seeing the rendered components is helpful, but we would have to look at the source code to find out how to use them without proper tooling.
- view the code that produces a specific component state;
- change the props and get an instant component update.
Data-dependent components are not that trivial to render in isolation. We might have to create mocks/stubs for API requests, inject data stores, and include services or data providers.
A common pattern is to decouple the component that fetches the data, typically called a smart component or a container, from the component that displays the data, typically called a dumb or a presentational component.
This way, we can easily render only the presentational component, passing any arbitrary data, without worrying about external data sources.Focus on the component's public interface
A strange thing happens when we develop components in isolation because we're putting ourselves in the consumer's shoes. We get to experience the component's API usage in all its state variations, helping us spot improper names or types.
To see the city skyline, we'd have to go out of the city. Likewise, to see what the Matrix is, we'd have to step out of the Matrix.
The same applies to component API design.
One last thing to keep in mind is that this technique is not an all-or-nothing decision. We can try it out on a small scale and gradually incorporate it if it proves to be helpful:
Having only a local development setup is a significant first step. We could use it on a case-by-case basis when it helps to isolate components either during development or debugging.
Sharing it with the team would the following step, where other team members could also benefit from the documented components and even contribute with their components as well.
Publishing a static build could benefit non-developers like testers or designers, allowing them to browse and experience the implementation of the existing components.