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.
Separation of Concerns (SoC)
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
Requirements
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.
The Analogy
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.
The Pagination Component
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:
- setCursor(newCursor:number):void;
- goNext():void;
- goPrev():void;
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:
- shouldShowArrows:boolean
- shouldShowBullets:boolean
(Bonus challenge: How would you approach having more pagination UIs here?)
The CardsDeck Component
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:
- cardsData:Card[];
- cursor
- transitionType:TransitionType;
(Bonus challenge: Should the CardsDeck validate that the given cursor is not out of bounds of the cards list length?)
Cards with dynamic content. How?
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.
Decoupling
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).
Composition
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.
The Auto Pagination
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:
- toggleAutoPagination():void
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 conclusion
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 🍻
Photo by Raphael Schaller on Unsplash
Top comments (5)
In your hook usePagination, why use a useReducer instead of useState?
Great question actually :)
As a matter of a fact I used the reducer to obtain control on the setCursor and do some validation for it (you can read more about it in details here: dev.to/mbarzeev/creating-a-react-c...)
After the comments here I went a little bit deeper and saw that it can also be done with useState (still not in the most elegant manner), and I updated my hooks package accordingly.
Thanks for the feedback!
According to the React Doc the useReducer is prefer over useState if you have a complex state logic or your next state depends on the previous one. The latter case is pagination.
Unfortunately this is not a good example of using a reducer. It could work for pagination, but you would place the next/previous/set logic inside of the reducer. In this case it's essentially used as useState.
great job articulating abstraction. well done!