React Advanced: Decoupling your components in the right way

Riccardo Tartaglia - Sep 24 '23 - - Dev Community

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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.
  2. 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));
Enter fullscreen mode Exit fullscreen mode

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 };
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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!

. . . . . . . . . .
Terabox Video Player