Join me in this post as I share with you my thought process when approaching component development. I will take a component and extract it into modular parts, explaining why each exists and how they all fit together at the end to form a solid yet flexible and maintainable result.
One of the most important aspects of programming in general, and component development in particular, is “Separation of Concerns” (or SoC). This design consideration can save so much trouble down the road, and it applies to any development challenge you might be facing. SoC basically means that each component has its own responsibilities which do not "leak" to other components.
For us FEDs it becomes more evident when creating components. Having a good SoC means that we can move components around, extend and reuse them easily. But is it enough to know how the component looks and acts in order to jump right in and start coding it? How do we know if our component has a good SoC?
I hope that this example I’m about to share with you here will clear things up a bit and help you better approach your component’s making.
Note: Though written with React in mind most of the concepts brought here are not confined to it and can be practiced with other frameworks
Our component is pretty simple at first glance. We have some sort of swapping content and we can paginate through it using arrows or clicking a specific page index to move directly to.
Here is rough wireframe sketch of it to help you imagine how it should look like:
But wait, let’s put some spice into it -
The pages should support 3 types of transition between them: fade in-out, sliding and flipping. The pagination on the other hand should support having just the arrows, having just the numbered bullets or not exiting at all.
The entire thing should also support auto pagination, where the pages swap automatically.
Oh and another thing - in case we’re on auto pagination, hovering the page will pause the transition.
Let it settle for a minute and let’s go :)
The naive approach is to put everything in the same component, a single file which holds the pages and the pagination, but we know that product requirements tend to change and so we would like to make sure our component is solid yet flexible as much as possible to support future changes without sacrificing the maintainability of it by making it extremely complex.
When you look at the component above it immediately cries out to separate it into 2 components - the Content and the Pagination.
Thinking about it, I decided to use a Cards Deck analogy here, which fits very well and will help me make the right decisions for each part’s responsibilities later on.
If the content is the cards deck, the pagination are the hands which go through the cards and select which card to show. Let’s keep that in mind as we go forward:
Deciding which “real life” analogy describes our component best is crucial to the process. The better you relate to the challenge at hand, the better your solution will be. In most cases dealing with “real life” examples makes it much easier to reason about than with abstract programming design ideas.
Having our analogy set we can proceed.
Let’s start from the bottom. What is the Pagination component?
A good approach is to think of a component outside the scope of the overall component we’re developing. What does the Pagination component do?
The Pagination component responsibility is simple - produce a cursor, that’s it.
If we take aside all the different ways it can produce this single cursor we realize that this component functionality comes down to this.
As a matter of a fact, the logic of producing the cursor can be encapsulated into a React hook, which has the following API:
Among the props this hook receives, it gets an
onChange(currentCursor:number) callback which is invoked whenever the cursor changes.
(You can see an example of such hook here)
The Pagination component simply uses this hook and renders a UI around it, with the required interactivity. Per our requirements the Pagination component should support the following props for now:
(Bonus challenge: How would you approach having more pagination UIs here?)
Like any cards deck you might know this component represents a stack of cards.
At this point it is really important to define your CardsDeck responsibilities.
The CardsDeck is basically a stack of cards. Does it know or care about what each card represents? Nope. It should receive a list of card data from outside (as a prop) and create a card for each.
However, it is concerned with how the cards are switched (transitioned) between them, so we understand that one prop of this component should be the type of transition we’re interested in. OUr CardsDeck should also receive a prop indicating which card should be shown now, that is - a cursor. It does not care what produced this cursor, it is “dumb” as can be. “Give me a cursor and I will display a card”.
Here are the props we currently have for it:
(Bonus challenge: Should the CardsDeck validate that the given cursor is not out of bounds of the cards list length?)
As stated before, the CardsDeck should not be aware of the content each card has, but still in order to manipulate the cards and transition between them it needs to have some kind of control over it. This means that the CardsDeck needs to wrap each content with a Card wrapper component:
But how do we enable having a dynamic rendered content when obviously the actual rendering of each card is done inside the CardsDeck component?
One option is using the render props, or “children as a function” approach - Instead of having a React element as a child of the CardsDeck we will have a function instead. This function will get the data of a single card (which is arbitrary) as an argument and return a JSX using that data.
In this way we are able to be very flexible as to how the content renders while maintaining the CardsDeck functionality.
Both the Pagination and the CardsDeck component are standalone components. They can reside in any other components and are totally decoupled from one another. This gives us a lot of power and allows us to reuse our code in more components, making our work much easier and more valuable.
This separation also gives us the ability to modify each in its own scope, and as long as the API is kept intact we can rely that the functionality of the components using it will not be harmed (putting visuals regression aside for now).
Once we have both components it is time to compose them together.
We put the CardsDeck and the Pagination inside a parent component. The CardsDeck and the Pagination component share the cursor and there we have it!
This composition allows us to play with how the CardsDeck and the Pagination are arranged and open more layout possibilities for the parent component. The parent component is also the place to determine whether to show the pagination or not.
What we have up until now kinda answers all our requirements except the last one, that is the auto pagination.
Here the real question rises - which component is responsible for managing the auto pagination?
We know that the CardsDeck is concerned with the transition type (slide, fade, etc.). Should it also be concerned with auto paginating them?
Let’s go back to our initial analogy - the cards deck and the hands.
If I ask you which is responsible for displaying one card after another the answer will be clear to you. These are the hands which are responsible for that, and not the cards deck.
So if we take it back to our component, it is clear that the Pagination component is the one responsible for it. To be more precise it is the part which is responsible for the logic behind manipulating the cursor - the Pagination hook.
We add another prop to our pagination hook which is
autoPaginate and if it is true it will start advancing the cursor automatically. Of course, if we have such a prop we need to also expose at least one more method from that hook, which will toggle the auto pagination on and off:
And now we need to bind the CardsDeck hover event with toggling the auto pagination. One option is to have our Pagination component expose a prop which determines whether to toggle the auto pagination on and off, and have it connected to a state on the parent component. That should do the trick.
In this post you saw how we can take a component, translate it to some “real life” example we can relate more to, and extract it into modular parts with a clear definition of concerns.
If you think about defining your components' boundaries better, your component will be much more easy to maintain and reuse, and in turn will make your and your product/ux team life a lot more pleasant.
As always, if you have other techniques you feel are relevant or any questions, please make sure to share them with the rest of us.
Hey! If you liked what you've just read check out @mattibarzeev on Twitter 🍻