Decoupling the logic from the UI in React using the BLoC pattern
Tags: javascript, react, bloc, mobx
Published: 2022-02-02 21:05:43

Software evolves and changes in time, and sooner or later this means that the library or framework used for the UI could be changed as well. The amount of work and refactoring needed when switching to another UI library or framework depends on how hard coupled is the code and business logic to it.

Writing React functional components can be fast and convenient, using hooks for the local state, business logic, etc. There are even libraries that makes it easier to write tests for our components, where the logic of the component can be tested. However, if in the future we were to change to another library, we would need to completely refactor all the components and the tests, spending more time on it than if we had the logic separated from the UI (in this case the React functional component).

In this article, I show one way of decoupling the logic from the UI using React functional components and MobX.

## Introduction to BLoC pattern.
BLoC stands for Business Logic Component, and was introduced by Google on the [DartConf 2018](https://www.youtube.com/watch?v=PLHln7wHgPE). The initial idea behind the BLoC pattern, was to decouple the business logic from the component so it could be reused between Flutter and Angular Dart.
The idea of separation of concerns between the view and it's logic has been around for many years, with other patterns like for example MVC (Model View Controller), MVP (Model View Presenter) and MVVM (Model View View-Model). BLoC would be the equivalent to the Controller in MVC, Presenter in MVP, and View-Model in MVVM. For a component based library like React, we would be using BLoC as the pattern for separating the business logic from the component UI. Some of the benefits that we would achieve by using this pattern are:

1. Better testability
It's easier to write tests only for the business logic of the component. Also it's great for TDD.

2. Components and screens logic become UI library/framework agnostic
Switching libraries becomes so much easier.

3. Reuse the BLoC in different components
A BLoC could be reused in components that share the same logic but have a different UI, not only for Web but also for React Native.

4. Extend the BLoC for similar components
BLoCs could extend other BLoCs that share the same base logic but add more features.

5. Cleaner code
The BLoC contains all the business logic and the Functional Component is only responsible of UI logic, like adding/removing CSS classes, conditionally rendering elements, subscribing to events and notifying the BLoC, etc. Which makes the component more readable and "thinner" (specially on components with more complex business logic)

## Implementing the BLoC pattern with MobX
MobX is a state management library that brings nonintrusive and transparent reactive functional programming to a standard JavaScript class. What this means is that when applied to normal JavaScript Classes, it makes properties and functions reactive without changing the way they are used. This is great, because it means that all the business logic is in a normal JavaScript class and the coupling to MobX is loose, allowing for an easier change of library if needed in the future.
MobX has bindings for the most popular libraries/frameworks like, React, Vue, Angular and Preact, so switching between any these libraries in the UI would not require any change in the BLoC.

The basic concepts of MobX are:
- `observable`: Holds and tracks a state value and informs of any change to it's subscribers
- `computed`: Returns a derived value from other states, which are being tracked so it can automatically recompute and inform of changes to it's subscribers
- `actions`: Used to update the observables (state)
- `observer`: Subscribes the Component to the `observables` and `computed` in the BLoC, making it rerender on a change.

The BLoC holds all the logic, properties and state of the component. The component sends events to the BLoC by calling a method (that would be normally internally configured as `action` if the method changes the state) and gets notified of changes through the component's `observer` that is subscribed to the BLoC's `observables` and `computed` properties, that hold the state.

![use-bloc diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n7atvi5qeh8hfj4db22y.png)

MobX has more advanced features like `reactions`, which are basically subscriptions to `observables` or `computed`, that can be useful for components or screens with more complex business logic.

## Installing MobX
Installing MobX requires 2 libraries, the `mobx` library which it's used in the BLoC to setup the observables, computed, actions, etc. and the UI bindings library that "glues" the BLoC to the component, which in this case since we are using functional components would be `mobx-react-lite`.

`npm i mobx mobx-react-lite`

## Using BLoC with React hooks
With this pattern, we want the BLoC object to be instantiated and kept durning the life of the component, updated on a rerender (or re-created), and disposed automatically (if needed) of any subscriptions, timers, references, etc. when the component it's unmounted.
For this we can use the [useBloc](https://www.npmjs.com/package/use-bloc) custom hook that I have published as a NPM package.
To install it: `npm i use-bloc`

It has 3 parameters:
- First, the BLoC constructor
- Second, the BLoC parameters (props)
- Third, an optional array of properties from the params that will re-create the BLoC (This will normally be params that are used for the state)

### Example
Let's create a simple Checkbox component that has an `isChecked` state, an optional `label` with some text to show and an optional `onChange` callback.
For this example we would be using TypeScript.

Apart from the libraries mentioned above it uses `BlocInterface` with the optional methods that `useBloc` expects:
#### BlocInterface (bloc-interface.d.ts)

```typescript
type BlocInterface<P> = {
  dispose?: () => void;
  updateParams?: (params: P) => void;
} & object;
```

It also uses a global TypeScript generic type for defining the default properties:
#### Global types (global.d.ts)
From [TypeScript globals.md](https://gist.github.com/jbaez/71df88c473629af75f11b404122fb09d)

```typescript
// Returns the optional keys of T as a union of string literals
declare type OptionalKeys<T> = Exclude<
  {
    [K in keyof T]: T extends Record<K, T[K]> ? never : K;
  }[keyof T],
  undefined
>;

// Makes all optional properties in T required
declare type OptionalParams<T> = Required<Pick<T, OptionalKeys<T>>>;

// Returns a Readonly type with all optional properties in T required
declare type OptionalDefaults<T> = Readonly<OptionalParams<T>>;

// Returns a Readonly type with all optional properties in T required excluding keys from K
declare type OptionalDefaultsPartial<T, K extends keyof T> = Omit<
  OptionalDefaults<T>,
  K
>;

```
#### BLoC (checkbox-bloc.ts):

```typescript
import { action, makeObservable, observable } from 'mobx';
import { BlocInterface } from './bloc-interface';

type OnChange = (checked: boolean) => void;

export interface CheckboxParams {
  checked?: boolean;
  label?: string;
  onChange?: OnChange;
}

const defaults: OptionalDefaultsPartial<CheckboxParams, 'onChange'> = {
  checked: false,
  label: '',
};

class CheckboxBloc implements BlocInterface<CheckboxParams> {
  isChecked: boolean;
  label: string = defaults.label;
  onChange?: OnChange;
  constructor(params: CheckboxParams) {
    this.isChecked = params.checked ?? defaults.checked;
    this.updateParams(params);

    makeObservable(this, {
      isChecked: observable,
      setChecked: action,
    });
  }

  updateParams(params: CheckboxParams) {
    this.label = params.label ?? defaults.label;
    this.onChange = params.onChange;
  }

  setChecked(checked: boolean) {
    this.isChecked = checked;
    if (this.onChange) {
      this.onChange(checked);
    }
  }
}

export default CheckboxBloc;
```

#### Component (checkbox.tsx)

```tsx
import React from 'react';
import CheckboxBloc, { CheckboxParams } from './checkbox-bloc';
import { useBloc } from 'use-bloc';
import { observer } from 'mobx-react-lite';

const Checkbox = observer((props: CheckboxParams) => {
  const bloc = useBloc(CheckboxBloc, props, ['checked']);
  return (
    <label>
      <input
        type="checkbox"
        checked={bloc.isChecked}
        onChange={(e) => bloc.setChecked(e.target.checked)}
      />
      {bloc.label}
    </label>
  );
});

export default Checkbox;
```

As we can see, the `Checkbox` component is wrapped into an `observer`, effectively subscribing the component to the `observable` and `computed` changes, which would cause a rerender.

The `isChecked` observable (state) from the `bloc` instance is passed into the input `checked` attribute.
When the `onChange` event is triggered, it calls the `setChecked` action in the `bloc` with the new value, which updates the `isChecked` observable, causing the Checkbox component to rerender updating the `checked` attribute.

A change to the `label` prop would also cause a rerender, and useBloc would call `updateParams` in the CheckboxBloc's instance updating it's `label` property from the updated props, so when the JSX is built, it would use the updated `label` from the `bloc` instance.

If the `checked` prop is updated, it would also cause a rerender, but since this prop was used in the 3rd parameter of `useBloc`, it would cause the CheckboxBloc instance to be re-created with the new `checked` value.

## Conclusions

Decoupling the business logic from the component might seem like extra work and additional libraries to install and learn, but the benefits in the long run are usually greater.

I find that MobX simplifies the implementation of the BLoC pattern making the state of the component reactive by just calling one method. The learning curve is easy as it's just a few concepts to understand.

It's great for TDD when defining the BLoC behaviour as you don't need to think about the component UI structure before writing the test, only the required behaviour.