Làm thế nào để cấu trúc các components trong React?

November 13, 2017 (6y ago)

Programming is quite a complex task. Especially crafting clean code is hard. We need to take care of many elements — naming variables, scoping functions, handling errors, ensuring security, monitoring performance etc. Still to name single most difficult thing in programming I would go with writing loosely coupled & highly cohesive components. It doesn’t matter if we’re talking about object-oriented or functional programming. Structuring system is the hardest thing and it has a big impact on the overall project. It takes years to become proficient in designing software architecture (& probably one can never really master it — in such a fast moving industry mastery is always one step ahead, there is always a way to improve design).

I really enjoy working with React & I think its biggest advantage is how simple React is. There is a difference between simple & easy https://www.infoq.com/presentations/Simple-Made-Easy. And I really mean React is simple. Of course, you need to spend some time to get to know it. But after you understand core concepts, everything else is just a consequence. The hard part comes next.

Coupling & Cohesion

Those are the metrics that more or less describe how difficult it will be to change the behaviour of the code. Coupling & cohesion are used in object-oriented programming and refer to some form of classes. We’ll use them in reference to React components since the same rules apply.

Coupling is connection or dependency between elements. If changing one element requires changing another element then we say there is tight coupling. If elements are loosely coupled, changing one element does not imply changes in the other. For example, let’s take a look at displaying bank transfer amount. If displaying amount knows how rates are calculated, then anytime internal structure of transfer changes, displaying code also needs to be updated. If we design system to be loosely coupled, based on the interface of an element, then changes to transfer shouldn’t result in changes to the view layer. Loosely coupled components are easier to manage and maintain.

Cohesion tells if element’s responsibilities form one thing. That metric is connected with Single Responsibility Principle or Unix principle: Do one thing and do it well. If account balance formatter also calculates interest rates and checks the permission to display history, then it has many responsibilities and those are not related to each other. Probably, there should be different components for permission handling or interest rates. On the other hand, if there are multiple components one for integer part, one for floating and one for currency, then anytime programmer wants to display balance, they would need to find all elements. The challenge is to create highly cohesive components.

Structuring components

There are many ways we can structure components. We want components to be reusable, but only to the degree that is reasonable. We want to build small components that can be used to build bigger concepts. Ideally, we want to build loosely coupled & highly cohesive components, so our system is easier to maintain and grow. In React components props can be treated like function arguments and that’s exactly the case for functional stateless components. How we define props in a component, defines how a component can be reused.

We’ll use expense manager domain and we’ll analyze expense details formatter. Let’s suppose that expense model looks like this:

There are several possibilities to model expense details formatter:

We’ll discuss each of them to see what are benefits and flaws of using each and every. Keep in mind that context is the king and everything depends on the system. That’s exactly what we’re paid for — building proper abstraction.

No props at all

The simplest solution & the one that is often the starting point is building a component with hard-coded data.

Passing no props, of course, doesn’t give us any flexibility and component is suitable to be used only in single place. Of course, in the example of expense details, we can see from the beginning that component needs to accept some props. Nevertheless, there are cases that components without any props is good solution. Firstly, we can use components without props for “constant” content like badges, logos, company info etc.

Building even small components makes a system more maintainable. Keeping information in one place allows making changes in one place. Don’t repeat yourself.

Passing expense object

In case of expense details definitely, we need to pass data to the component. First, we’ll take a look at passing expense object.

Passing expense object to expense details component makes perfect sense. Expense details formatter is highly coherent -> it displays data of expense. Whenever we want to change formatting, this is the only place that’s going to change. Also changing expense details formatter does not introduce any side effects to expense object itself.

The component is tightly coupled to expense object. Is that a bad thing? Definitely not, but we must be aware how that influences our system. Passing expense object as props, results that expense details component relies on the internal structure of expense. Whenever we change the internal structure of expense, we’ll also need to change expense details. Of course, we’ll only need to make changes in one place.

How does that design affect future changes? If we want to add, change or remove a field, we’ll only need to change one component. What if we want to add different date formatters? We could add another prop for date formatting.

We start adding additional properties to make the component more flexible. As long as there are only a few options, everything is great. The problem starts after system grows and we have a lot of props for different use cases.

const ExpenseDetails = ({ expense, dateFormat, withCurrency, currencyFormat, isOverdue, isPaid ... })

Adding props makes the component more reusable, but it can also be a sign that there are multiple responsibilities of the component. The same rule applies to the function. We can create a function with a number of parameters, but as soon as that number is greater than 3–4, it starts to do a lot of things. And probably that’s the time to split function into smaller one.

As number of component props grow, we can decide to split component into more defined ones like: OverdueExpenseDetails, PaidExpenseDetails etc.

Passing only required properties

To be less coupled with expense object itself, we can pass only required properties.

We’re passing each and every property separately, so we’re moving the responsibility a bit to one who is using component. If internal structure of expense changes, it’s not affecting expense details formatter itself -> but probably it can affect every place that is using component because props need to be changed. When passing props as separate properties, a component is more abstract.

How passing only required fields affect future design? Adding/updating/removing fields is not easy now. Whenever we want to add a field, we not only need to change the implementation of expense details but also change every place where component is used :(

const ExpenseDetails = ({ category, description, amount, date, account, comment, case ... }) => ( ... )

On the other hand, supporting multiple date formatting is done almost out-of-the-box. Since we’re passing date as a prop, we can pass formatted date.

Deciding how to display particular field is in the hands of the one who uses the component. That is no longer the case of expense details component implementation.

Passing map/array of properties

Going even more abstract, we could pass a map of properties.

The one who uses component is in control over formatting expense details. The object passed to the component has to be properly formatted.

That solution has many flaws. We have very little control over how the component will look. The order of reduce is not specified, so we’ll need to add some kind of order. Instead of a map, we could pass an array with objects to overcome that problem, but it still will have drawbacks.

Passing map/array as props is not coupled to expense at all but is also not coherent at all. Adding/removing new properties is only a matter of changing prop, but we have no control over the formatting of the component itself. If we want to change only the formatting of the category, it’s not possible it this solution. (To be precise, there is always a way to tweak stuff. For example by passing another props with formatting config. Yet that solution is no longer clean and straightforward.)

Passing format as a child

We could also take as little responsibility as possible and pass data as a child.

In that case, expense details is only a container to provide some structure and styling. To display details the one using component has to provide all information.

Probably in case of expense details, it’s not a good solution, since we’ll need to repeat a lot. Still, flexibility is huge and there are a lot of different formatting possibilities. Adding/removing/updating fields is only a matter of changing the use of the component. The same goes with date formatting. We lose coherence, but that’s the price we had to pay.

Context is the king

As you can see, we’re exchanging different advantages and possibilities. Which one is the best? It depends on:

There is no single good solution. One size doesn’t fit all. How we structure our components has a great impact on how we’ll maintain a system and how expandable it will be. It all depends on the context. Thankfully we have plenty of options and we can pick and choose. Components are a great abstraction to build both small and big systems. It’s only a case of picking right solution.