Every developer's dream is to write less code and, if possible, make it all reusable.
In React, this translates into knowing how to properly decouple the logic of a component from its presentation.
Easier said than done, right?
In this article, I'll show you how to effectively decouple your components to make your code extremely reusable.
Before we get started, let's take a look at the fundamental concept of "coupling."
Coupling
In computer science, coupling is a concept that denotes the dependence between two or more components. For instance, if a component A
depends on another component B
, then A
is said to be coupled with B
.
Coupling is the enemy of change because it ties together things that can change in parallel.
This makes it extremely challenging to modify a single point in your application. Touching a single component can introduce anomalies in various parts of the application.
You must spend time tracking down all the parts that need to be modified, or you'll find yourself wondering why everything has gone haywire.
If we view a React component as a pure presentation element, we can say that it can be coupled with many things:
- Business logic that determines its behavior (hooks, custom hooks, etc.).
- External services (APIs, databases, etc.).
- Another React component (for example, a component responsible for managing the state of a form).
This tight coupling, when modified, can lead to unpredictable side effects on other parts of the system.
Let's take a closer look at this component.
import { useCustomerHook } from './hooks';
const Customer = () => {
const { name, surname } = useCustomerHook();
return (
<div>
<p>{name}</p>
<p>{surname}</p>
</div>
);
};
It all seems fine at first glance, but in reality, we have a problem: this component is coupled with the custom hook useCustomerHook, which retrieves customer data from an external service. Therefore, our Customer component is not a "pure" component because it depends on logic that is not solely related to presenting its UI.
Now, let's consider that the custom hook useCustomerHook is also used in other components. What can we expect if we decide to modify it? Well, we should brace ourselves for quite a bit of work because we'll have to modify all the components that use it and are coupled with it.
Decoupling the Logic of a React Component
Let's revisit the previous example. I mentioned that the Customer component is coupled with the custom hook useCustomerHook, which applies fetching logic to retrieve customer data.
Now, let's explore how to decouple the logic of this component, so that we can transform it into a pure presentation component.
import { useCustomerHook } from './hooks';
const Customer = ({name, surname}) => {
return (
<div>
<p>{name}</p>
<p>{surname}</p>
</div>
);
};
const CustomerWrapper = () => {
const { name, surname } = useCustomerHook();
return <Customer name={name} surname={surname} />;
};
export default CustomerWrapper;
Now, the Customer component is indeed a pure presentation component because it no longer uses useCustomerHook and only handles UI logic.
I've employed a wrapper component to decouple the logic of the Customer component. This technique is known as Container Components and allows us to modify the UI of our component without worrying about "breaking" the underlying logic.
Customer now only needs to concern itself with displaying presentation information. All the necessary variables are passed as props, making it easy to nest it anywhere in our code without concerns.
However, I'm still not satisfied for two reasons:
- The CustomerWrapper component is still coupled with the custom hook. So, if I decide to modify it, I would still need to modify the wrapper component.
- I had to create an extra component, CustomerWrapper, to decouple the logic, which means I've written a bit more code. We can address these two issues by using composition.
Composition
In computer science, composition is a concept that refers to combining two or more elements to create a new one. For example, if we have two functions f
and g
, we can compose them to create a new function h
, which is the composition of f
and g
.
const f = (x) => x + 1;
const g = (x) => x * 2;
const h = (x) => f(g(x));
We can apply the same concept to custom hooks as well. In fact, we can compose two or more custom hooks to create a new one.
const useCustomerHook = () => {
const { name, surname } = useCustomer();
const { age } = useCustomerAge();
return { name, surname, age };
};
In this way, the custom hook useCustomerHook is composed of the custom hooks useCustomer and useCustomerAge.
By using composition, we can decouple the logic of a React component without having to create a wrapper component. To apply composition conveniently, we use the library react-hooks-compose
Let's see how we can apply composition to our example.
import composeHooks from 'react-hooks-compose';
import { useCustomerHook } from './hooks';
const Customer = ({name, surname}) => {
return (
<div>
<p>{name}</p>
<p>{surname}</p>
</div>
);
};
export default composeHooks({useCustomerHook})(Customer);
Now, the Customer component is indeed a pure presentation component. It's not coupled with any custom hooks and only handles UI logic. Furthermore, you didn't have to create any additional components to decouple the logic. In fact, composition allows you to create a cleaner and more readable component.
Another strength of this technique lies in how easy it makes testing the Customer component. You don't need to worry about testing the business logic; you only need to test the UI logic. Additionally, you can test the custom hooks separately.
As a final highlight, let's see what happens if you decide to add a new custom hook that adds some logic to the Customer component, such as a custom hook that handles logging customer information.
import composeHooks from 'react-hooks-compose';
import { useCustomerHook, useLoggerHook } from './hooks';
const Customer = ({name, surname, age}) => {
return (
<div>
<p>{name}</p>
<p>{surname}</p>
</div>
);
};
export default composeHooks({
useCustomerHook,
useLoggerHook
})(Customer);
Perfect, you've added the custom hook useLoggerHook to the Customer component without having to modify the component itself.
This is because useLoggerHook was composed with the useCustomerHook.
Conclusions
In this article, we've explored how to decouple the logic of a React component through hook composition, turning it into a pure presentation component. The art of hook composition provides us with a powerful tool to enhance the modularity and maintainability of our React components.
By clearly separating business logic from presentation, we make our components more readable and easier to test. This methodology promotes code reusability and the scalability of React applications, allowing developers to focus on creating cleaner and more performant components.
If you have any suggestions or questions on this topic, please feel free to share them in the comments below. And if you found this article helpful, don't forget to share it with your fellow developers!
Top comments (20)
This creates a different type of dependency though, right? Each of the hooks have to return an object so that they can be destructured into a final composite object, which means you can’t have a hook that returns an array. And if two of the hooks have the same key then one will get overwritten. Still, it’s a very cool idea and I can see how useful it is. Thanks for the article!
Yes, your observation is right, but using Typescript (not used in this article) can mitigate these "issues".
Thanks for your feedback.
Thank you for writing and sharing this article. I have a question though:
Do we need an external lib to compose hooks? In order to reduce bundle size and dependence on external components, can we do this ourself?
While reading through your article, dependency injection kept coming to mind. Is it by any chance related to decoupling business logic from ui components?
You can write a simple reduce function for the hooks execution. Take a look to this: github.com/helloitsjoe/react-hooks...
tl,dr; yes, dependency injection is similar to higher-order functions :)
One of the benefits of DI is decoupling. This is made possible by following “program to an interface, not imementation”. Moreover, we follow another good coding practice “dependency inversion principle”.
Great question!
If I use useState and useEffect inside
<Customer/>
will it still be pure component ?Yes, if you managed only presentation logic :)
What if hook depend on an value from another hook ?
Maybe you can compose this two hooks in a new one.
Hooks are "functions" to manage business logic in React, it's ok that one hook depends from one other, it's a problem if an hook depends from five others hooks
This article is about high order components HOC, the are different way to decoupling the logic like compound components, context API, etc..
In reality it is the High Order Function pattern (HOC was based on the same pattern) applied to the hooks philosophy.
As you say there are many ways for decoupling your components :)
Glad to know about this.
Is this method considering the unneccessary re-render based on component tree structure?
I am not sure about this. Please let me know. Thanks
This method is only a way to separate logic from UI, don't optimize re-renders in the tree structure
Thank you for the article. How to do without the library react-hooks-compose library?
The source is pretty short - it wouldn't be terrible to just grab that and integrate it into your project source.
github.com/helloitsjoe/react-hooks...
"The source code is pretty short - it wouldn't be terrible to just grab that and integrate it into your project source [with respect to the license.]"*
Please please always look at the license and respect the effort people spent especially for Open Source projects licensed under MIT and similiar. It cannot be more demotivating providing open source code and reading someone writing "just grab the code". :s
And if there is no license, which is the case for react-hooks-compose, then you simply cannot and should not use the source code at all: opensource.stackexchange.com/a/1721.
You can use a simply reduce function for the serial esecution of the hooks
what theme is used in the preview?)
I've used ray.so to generate the preview :)
Nice article indeed. Some big problem using Typescript.