React coupling, decoupling and composition explained

React coupling, decoupling and composition explained
Photo by Vows on the Move / Unsplash

When working in React, you want to separate logic from your components, reuse those components as much as possible, and do that by writing as little code as possible.

This is easier said than done, but knowing how to separate business logic code from your presentation code is extremely important.

In programming, there are 3 concepts you should be familiar with or at least heard about them:

  1. Coupling
  2. Decoupling
  3. Composition

We will go through each of them with examples.

Coupling

Coupling is a concept that defines a dependence between two or more domains. For example, if you have ComponentOne which depends on ComponentTwo then we can say that ComponentOne is coupled with ComponentTwo.

Coupling is something you want to avoid in React because it makes your life harder when implementing new changes. Changing some parts of code in a dependent component can cause a domino effect on other coupled components, introduce bugs in other application parts, and cause all sorts of problems.

Coupling in React can be achieved in multiple ways:

  1. Dependency on some 3rd party, like APIs, databases, etc
  2. Some external business logic that dictates how a component behaves (eg custom hook)
  3. Child/Parent component, for example, Input component and parent component with the state for that Input component
  4. Other...

So when you have this coupling, any change can reflect unexpected behavior. Here is an example:

import { usePokemon } from './hooks';

const Pokemon = () => {
  const { name, height, weight } = usePokemon();
  return (
    <section>
      <p>Name: {name}</p>
      <p>Height: {height}</p>
      <p>Weight: {weight}</p>
    </section>
  );
};

On the first look, this component looks fine.

But if we analyze it a bit, we can see that it's dependent on a custom hook usePokemon. This is the No. 2 on the list above.

So this component is dependent on a custom hook because it gets the data from an external API service. That data is provided by usePokemon hook. Because of this, the Pokemon component is not a pure presentational component.

If we modify the code inside usePokemon hook, it will reflect on this component. It will maybe also reflect on other components that use that hook (domino effect).

So we should be careful with changes because we must fix all the other components that use this hook after the changes.

Decoupling

Now that we are familiar with the term coupling, let's fix this component and decouple it.

Here is what we can do:

import { usePokemon } from './hooks';

const Pokemon = ({name, height, weight}) => {
  return <section>
    <p>Name: {name}</p>
    <p>Height: {height}</p>
    <p>Weight: {weight}</p>
  </section>;
}

const PokemonCard = () => {
  const { name, height, weight } = usePokemon();
  return <Pokemon name={name} height={height} weight={weight} />;
};

export default PokemonCard;

The Pokemon component is now a pure presentational component.

We have a container component called PokemonCard that uses usePokemon hook and passes the data through the props.

This is a step in the right direction, but still not good for 2 reasons:

  • we have moved hook usage one level above, in the container component. If the hook is modified, we must fix the code in that component.
  • we created an additional container component, to decouple the logic. So we added more code to our project.

So, how can we fix these 2 issues?

Composition

Composition is a concept where you can combine 2 or more domains and create a new one.

If you watched Dragon Ball Z, when Goten and Trunks fused into Gotenks to beat Majin Buu. Well, composition is a similar concept.

Here is an example in the code:

const goten = (power) => power + 5;
const trunks = (power) => power + 5;
const gotenks = (power) => goten(trunks(power));

We can apply this concept to our hook as well because our custom hook is nothing more than a function:

const usePokemon = () => {
  const { name } = usePokemonBasic();
  const { height, weight } = usePokemonStats();
  return { name, height, weight };
};

With this, we created a new custom hook that combines 2 other hooks and uses only what's needed from those hooks.

To use this new hook, we can use an npm package react-hooks-compose or write your custom hook to do this.

Let's see the code now:

import composeHooks from 'react-hooks-compose';
import { usePokemon } from './hooks';

const Pokemon = ({name, height, weight}) => {
  return <section>
    <p>Name: {name}</p>
    <p>Height: {height}</p>
    <p>Weight: {weight}</p>
  </section>;
}

export default composeHooks({usePokemon})(Pokemon);

The Pokemon component is now a pure presentational component. We also don't have to worry about changing stuff in the hook and breaking some components below. We also didn't create any new container components to decouple the logic.

As you can see, composition allows you to create cleaner components. It also makes testing easier for you, as you can test only the presentational parts and not worry about logic. You can test your custom hook separately.

You can also combine other hooks with this approach:

import composeHooks from 'react-hooks-compose';
import { usePokemon, usePokemonFight } from './hooks';

const Pokemon = ({name, height, weight}) => {
  return <section>
    <p>Name: {name}</p>
    <p>Height: {height}</p>
    <p>Weight: {weight}</p>
  </section>;
}

export default composeHooks({usePokemon, usePokemonFight})(Pokemon);

That's it, it's simple and easy to implement.

With this approach, we successfully decoupled our components and separated business logic from our presentation code.

This has multiple benefits, less code, easier testing, and more reusable components.