Using DDD with Recoil
Tags: react, recoil, typescript, ddd
Published: 2023-01-22 11:26:19

---
title: Using DDD with Recoil
published: true
description: How to design Recoil state layer combined with your business logic.
tags: react,recoil,typescript,ddd
---

## Introduction

The following repository code is presented as an "Interactive Tutorial" on the official Recoil website.

https://github.com/SideGuide/recoil-example

I will try to rewrite this code using the design method I use in my published application. The target readers are those who have already read the Getting Started and Tutorial of Recoil. Therefore, I will not provide a basic explanation of Recoil in this article.

I designed my application with two things in mind.

1. Ensure domain layer independence
1. Concerns about rendering performance

[My application](https://harry0000.github.io/last-origin-unit-viewer/) has an ability to add and subtract values by holding down a button. So rendering time for a single change is less than 20ms, even in development mode.

When actually running in development mode, the screen is rendered as follows.

{% embed https://twitter.com/harry0000jp/status/1606582520400973826 %}

![Performance tab in DevTools](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e79vh65qt9mrkydo5dab.png)

### Assumptions

- Using TypeScript
- Domain logic has no asynchronous processing, only pure processing

### All of the artifacts in this article

https://github.com/harry0000/recoil-todo-app

<dl>
  <dt><code>original</code> tag</dt>
  <dd>Code copied from the original repository and minimally typed in TypeScript</dd>
  <dt><code>refactored</code> tag</dt>
  <dd>Code that reflects the changes made in this article</dd>
</dl>

## 💥 Problems with this Todo app

Let's try to run this Todo application with a new project and minimal typing in TypeScript. Then all components will be re-rendered just by changing the checkbox of an item. There is no point in using Recoil.

![Behavior before refactoring](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/01rn4v37f8n1ewzsqya9.png)

In addition, because it is simplified sample code, the domain logic is not cohesive and is not easy to test and maintain.

## 🛠 Let's refactor

### 1. Cohesive domain logic

Put together our domain logic in the `domain` directory.

The important point here is that Recoil expects immutable values to be stored, as evidenced by [the fact that it freezes the values stored in development mode](https://recoiljs.org/docs/api-reference/core/atom). Therefore, if you are building object-based domain code, implement it so that it returns a new object when the result of the logic execution changes, and if it is class-based, implement it as an immutable class.

The object-based implementation was used in the original Todo application and is probably already familiar to front-end engineers, so this time I will try a class-based implementation. I will keep the function names in the original code as much as possible, and aggregate the domain logic.

- [src/domain/TodoItem.ts](https://github.com/harry0000/recoil-todo-app/blob/refactored/src/domain/TodoItem.ts)
- [src/domain/TodoItemContainer.ts](https://github.com/harry0000/recoil-todo-app/blob/refactored/src/domain/TodoItemContainer.ts)

When testing these domain codes, you simply write test code similar to testing a class.

### 2. Implementing the Recoil state module

Now that the domain logic is implemented, all we need to do is store it in an `atom`, get the value with a `selector`, and we're done?

However, this is not the case, and the `get` of the `selector` is [re-evaluated when the dependent `atom` is updated](https://recoiljs.org/docs/api-reference/core/selector#dynamic-dependencies), which can cause unnecessary re-rendering. If the type of the value to get is of a type (e.g. object, array) whose equivalence cannot be determined by the strict equality operator (`===`), and where the cached value is not expected to be used by the `selector`, you should generally consider using `selector` for values that always change when the dependent `atom` is updated.  However, it is a bit odd that the state layer always needs to know if the value has changed after the domain logic is executed. Also, as in the case of this Todo application, it seems to me that it is a rare case where all values change after a particular domain logic is executed.

My application uses objects like `{ value: number }`, `{ milliValue: number }`, and `{ microValue: number }` in the domain logic, which cannot determine equivalence of values with `===` operators and in many cases I cannot expect the `selector` cache to be used.

#### Implementation policy

What is the policy for implementation?

1. Prepare separate Recoil state values to store the values referenced by each component.
1. Export only two types of elements from the state module.
    1. `RecoilValueReadOnly` to reference the state
    1. Callback functions to update the state
1. When the callback is invoked, the domain logic is executed and each value is stored in the state prepared in point 1.

In one sentence, the states referenced by each component are prepared separately, and the state update logic is hidden in the state module.

The reason for doing it like point 1 is to use the state only where it is displayed, as in the following video thumbnail. This ensures that only components that reference values that have actually changed are re-rendered.

{% embed https://www.youtube.com/watch?v=_ISAA_Jt9kI %}

However, if you export these `RecoilState` as they are, `useRecoilState()` can be called on any component, and the state can be changed from anywhere and in any way, and the integrity of the state cannot be guaranteed. At least for now, Recoil does not have the ability to change other `atom`s in response to a change in a particular `atom`.

So, using TypeScript, cast `RecoilState` to the type `RecoilValueReadOnly` and export it. Now any component cannot call anything on this state except `useRecoilValue()`.

My personal practice is to give a short, free name in high context to the states that are not exported, but to export states I use a name that can be understood by external modules, and I always add the suffix `State`.

- Example:

```ts
const _foo = atomFamily<boolean, number>({
  key: 'somestate_foo',
  default: false
});

export const fooState: (id: number) => RecoilValueReadOnly<boolean> =
  _fooState(id);
```

This time we need to prepare these states:

- Text and completion status for each todo
- Filter for todo list
- List of filtered todo lists
- Total number of todos
- Total number of completed todos
- Total number of incomplete todos
- Percentage of completed todos
- Text list of incomplete todos

Each state requires an initial value defined in the domain logic. The state is updated by a callback that is exported separately. Basically, it is someone (or something) outside the state module that executes the trigger that changes the state, so we provide the means to do so by exporting a callback.

Recoil provides a way to get/set the state with a callback. This callback is actually used via `useRecoilCallback()` in components and custom hooks, so it needs to be defined with a type like `CallbackInterface => (...any[]) => any`.

See also: https://recoiljs.org/docs/api-reference/core/useRecoilCallback/

In this case we would define a callback of the following type:

```ts
{
  addTodoItem: (cbi: CallbackInterface) => (text: string): void;

  editTodoItemText: (cbi: CallbackInterface) => (id: TodoItemId, text: string): void;

  toggleTodoItemCompletion: (cbi: CallbackInterface) => (id: TodoItemId): void;

  deleteTodoItem: (cbi: CallbackInterface) => (id: TodoItemId): void;

  updateFilter: (cbi: CallbackInterface) => (filter: TodoListFilter): void;
}
```

It is possible to get the state via `snapshot` of `CallbackInterface` and update the state via `set` / `reset`.

- Type definition of `CallbackInterface`:

```ts
type CallbackInterface = {
  snapshot: Snapshot,
  gotoSnapshot: Snapshot => void,
  set: <T>(RecoilState<T>, (T => T) | T) => void,
  reset: <T>(RecoilState<T>) => void,
  refresh: <T>(RecoilValue<T>) => void,
  transact_UNSTABLE: ((TransactionInterface) => void) => void,
};
```

- Example of getting a value using a `snapshot`:

Since it is assumed that there is no asynchronous processing in the domain logic, you can get the value synchronously from the state.

```ts
import { GetRecoilValue } from 'recoil';

const get: GetRecoilValue = (recoilVal) => snapshot.getLoadable(recoilVal).getValue();

const someValue = get(someState);
```

#### Actual implementation code

There are two possible implementations of the state module: value/function-based and class-based.

<dl>
  <dt>value/function-based</dt>
  <dd>Value and function are defined at the top level of the module</dd>

  <dt>class-based</dt>
  <dd>Defining a class and exporting its singleton instance</dd>
</dl>

Since the original Todo application is value/function-based and there is nothing unusual about it, I want to implement it as class-based this time. In the case of class-based, since the recoil state is defined in the field of the class, only a single instance is exported since the definition of the state (key) will be duplicated if multiple instances are created.

- src/state/recoil_state.ts

```ts
class TodoState {
  // ...
}

export const todoState = new TodoState();
```

It would be more appropriate to rename the file to `TodoState.ts`, but I left it as is.

If you want to implement a value/function-based module, move each field of the class to the top level of the module and export the field that was public. Note that you cannot use `#` as a variable prefix, you must change it to `_` or something similar.

As for the state that each component refers to, just cast the defined one with `RecoilValueReadOnly` and export it as described above.

For the todo list, detailed information about each todo is obtained from the end components that display it, so its type is defined as `atom<ReadonlyArray<TodoItemId>()` and the id is passed as props in the component. Then the todo text and completion state are held in `atomFamily<string, TodoItemId>()` and `atomFamily<boolean, TodoItemId>()` respectively.

The code for the definition section is omitted here, but one point should be mentioned: code completion may be easier if state is defined in meaningful block units.

- [src/state/recoil_state.ts#L58-L61](https://github.com/harry0000/recoil-todo-app/blob/refactored/src/state/recoil_state.ts#L58-L61)

```ts
  readonly #items = {
    text: atomFamily<string, TodoItemId>({ key: 'TodoState.#items.text', default: TodoItem.initialState.text}),
    completion: atomFamily<boolean, TodoItemId>({ key: 'TodoState.#items.completion', default: TodoItem.initialState.isComplete})
  };
```

Now let's write the process of storing a value in state when each callback is invoked. We can simplify the implementation of all callbacks by preparing an `atom` that stores the domain object and creating a `#updateState` like this:

```ts
class TodoState {

  readonly #itemContainer = atom({
    key: 'TodoState.#itemContainer',
    default: TodoItemContainer.create()
  });

  // ...

  #updateState = ({ set, reset, snapshot }: CallbackInterface) => (updater: (state: TodoItemContainer) => TodoItemContainer): void => {
    // TODO: implement
  }

  // ...

  readonly addTodoItem = (cbi: CallbackInterface) => (text: string): void => {
    this.#updateState(cbi)(state => state.addItem(text));
  };

  readonly editTodoItemText = (cbi: CallbackInterface) => (id: TodoItemId, text: string): void => {
    this.#updateState(cbi)(state => state.editItemText(id, text));
  };

  readonly toggleTodoItemCompletion = (cbi: CallbackInterface) => (id: TodoItemId): void => {
    this.#updateState(cbi)(state => state.toggleItemCompletion(id));
  };

  readonly deleteTodoItem = (cbi: CallbackInterface) => (id: TodoItemId): void => {
    this.#updateState(cbi)(state => state.deleteItem(id));
  };

  readonly updateFilter = (cbi: CallbackInterface) => (filter: TodoListFilter): void => {
    this.#updateState(cbi)(state => state.setFilter(filter));
  };
}
```

All that remains is to implement `#updateState`. Since we implemented the domain object in the immutable class, we can `set` the updated value without thinking about anything in particular. Also, if the type of the state can be determined to be equivalent with the `===` operator, it can be `set` as is, since update propagation and re-rendering will not be performed if it is the same as the previous value.

The problem is with arrays, objects, etc., where even if the values are equivalent, if the references are different, the comparison with the `===` operator will result in `false` and state update propagation and re-rendering will occur. So, use `fast-deep-equal` to compare with the previous value and `set` only if there is a change. Unless it is a very special case, the cost of a `fast-deep-equal` comparison should be less than the cost of a re-rendering without it.

In addition, when deletions may occur as a result of updates, as in this Todo application, [it is necessary to `reset` the state](https://github.com/facebookexperimental/Recoil/issues/622) by comparing it to the previous state and identifying the deleted items.

These points should be noted and implemented:

- [src/state/recoil_state.ts#L63-L97](https://github.com/harry0000/recoil-todo-app/blob/refactored/src/state/recoil_state.ts#L63-L97)

```ts
  #updateState = ({ set, reset, snapshot }: CallbackInterface) => (updater: (state: TodoItemContainer) => TodoItemContainer): void => {
    const get: GetRecoilValue = (recoilVal) => snapshot.getLoadable(recoilVal).getValue();

    const prevState = get(this.#itemContainer);
    const nextState = updater(prevState);
    set(this.#itemContainer, nextState);

    set(this.#filter, nextState.filter);

    const prevFilteredIds = get(this.#filteredItemIds);
    const nextFilteredIds = nextState.filteredItems.map(({ id }) => id);
    if (!deepEqual(prevFilteredIds, nextFilteredIds)) {
      set(this.#filteredItemIds, nextFilteredIds);
    }

    set(this.#numOfTotal, nextState.numOfTotal);
    set(this.#numOfCompleted, nextState.numOfCompleted);
    set(this.#numOfUncompleted, nextState.numOfUncompleted);
    set(this.#percentCompleted, nextState.percentCompleted);

    const prevTexts = get(this.#uncompletedTexts);
    const nextTexts = nextState.uncompletedItems.map(({ text }) => text);
    if (!deepEqual(prevTexts, nextTexts)) {
      set(this.#uncompletedTexts, nextTexts);
    }

    getDeletedItemIds(prevState, nextState).forEach(id => {
      reset(this.#items.text(id));
      reset(this.#items.completion(id));
    });
    nextState.items.forEach(({ id, text, isComplete }) => {
      set(this.#items.text(id), text);
      set(this.#items.completion(id), isComplete);
    });
  }
```

- 🚨 Another thing to note is that the value you can get from a `snapshot` during a callback call is always the same. That is, if you get a value via `snapshot` after `set`, it will not be updated to the value you just `set`, but will remain the same as it was before `set`.

### 3. Implementing Components

Finally, we are ready to use the Recoil state in components.

In this example code, I will call `useRecoilValue()` and `useRecoilCallback()` directly in the component, but you can create custom hooks if you want.

#### Referring to the state

If you want to refer to a state like the thumbnail in the official Recoil video, refer to it in the node (component) at the end of the DOM tree as much as possible to display it.

In this Todo application, the code in `TodoListStats.tsx` is the simplest example:

- [src/component/TodoListStats.tsx](https://github.com/harry0000/recoil-todo-app/blob/refactored/src/component/TodoListStats.tsx)

```ts
import React from 'react';
import { useRecoilValue } from 'recoil';

import { todoState } from '../state/recoil_state';

const {
  todoListTotalState,
  todoListTotalCompletedState,
  todoListTotalUncompletedState,
  todoListPercentCompletedState,
  todoListNotCompletedTextsState
} = todoState;

const Total = () => {
  const totalNum = useRecoilValue(todoListTotalState);

  return (<span>{totalNum}</span>);
};

const TotalCompleted = () => {
  const totalCompletedNum = useRecoilValue(todoListTotalCompletedState);

  return (<span>{totalCompletedNum}</span>);
};

const TotalUncompleted = () => {
  const totalUncompletedNum = useRecoilValue(todoListTotalUncompletedState);

  return (<span>{totalUncompletedNum}</span>);
};

const FormattedPercentCompleted = () => {
  const percentCompleted = useRecoilValue(todoListPercentCompletedState);
  const formattedPercentCompleted = Math.round(percentCompleted * 100);

  return (<span>{formattedPercentCompleted}</span>);
};

const UncompletedTexts = () => {
  const texts = useRecoilValue(todoListNotCompletedTextsState);

  return (<span>{texts.join(' ')}</span>);
};

const TodoListStats = () => {
  return (
    <ul>
      <li>Total items: <Total /></li>
      <li>Items completed: <TotalCompleted /></li>
      <li>Items not completed: <TotalUncompleted /></li>
      <li>Percent completed: <FormattedPercentCompleted /></li>
      <li>Text not completed: <UncompletedTexts /></li>
    </ul>
  );
};

export default TodoListStats;
```

### Update the state

Just get the callback with `useRecoilCallback()` and call it when you need it.

- [src/component/TodoItemCreator.tsx](https://github.com/harry0000/recoil-todo-app/blob/refactored/src/component/TodoItemCreator.tsx)

```ts
import React, { ChangeEventHandler, useState } from 'react';
import { useRecoilCallback } from 'recoil';

import { todoState } from '../state/recoil_state';

function TodoItemCreator() {
  const [inputValue, setInputValue] = useState('');
  const addTodoItem = useRecoilCallback(todoState.addTodoItem);

  const addItem = () => {
    addTodoItem(inputValue);
    setInputValue('');
  };

  const onChange: ChangeEventHandler<HTMLInputElement> = ({ target: { value } }) => {
    setInputValue(value);
  };

  return (
    <div>
      <input type="text" value={inputValue} onChange={onChange} />
      <button onClick={addItem}>Add</button>
    </div>
  );
}

export default TodoItemCreator;
```

#### One last push

When I check the behavior after changing all the components, I notice that each row of the todo list is re-rendered when a todo is added, for example. It seems that the `TodoItemList` component in `TodoList.tsx` updates the filtered todo list when it renders. Currently the `TodoItem` component only gets `TodoItemId` in its props, so we can suppress the re-rendering by memoization.

- [src/component/TodoItem.tsx#L41-L49](https://github.com/harry0000/recoil-todo-app/blob/refactored/src/component/TodoItem.tsx#L41-L49)

```ts
const TodoItem: React.FC<{ itemId: TodoItemId }> = React.memo(({ itemId }) => {
  return (
    <div>
      <TodoItemTextField itemId={itemId} />
      <TodoItemCompletionCheckbox itemId={itemId} />
      <TodoItemDeleteButton itemId={itemId} />
    </div>
  );
});
```

## 🎉 After refactoring

With all the changes made, let's see how it works. Unlike the original Todo application, only the components that reference the updated values should be re-rendered.

![Behavior after refactoring](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gf762jolqyoyb78xy2g0.png)

## Conclusion

In this article, I have presented an example of a design that combines independent domain layer and Recoil so that only components that actually refer to the changed values are re-rendered. I hope this article will be helpful to anyone reading it for design ideas. And please share your own Recoil state layer design patterns with me!

## Appendix

### ⚠ Notes on class-based state module

For example, when other singleton instances are referenced in a class, if the references are interdependent, a deadlock occurs and the creation of that singleton instance fails, resulting in a runtime error.

![Instance creation deadlock due to singleton instance cross-reference](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jnhvxwhqnm8ov4d8s9xb.png)

Therefore, if you want to use the updated values of other modules in a particular state module to perform update processing, you will have to be creative.

The other caveat is that you should always implement all members as fields. If implemented as getter or method, using a singleton instance as follows will cause an error at runtime because `this` in the getter or method will be `undefined`.

```ts
import { fooState } from './state/FooState';

const {
  oneState,
  twoState,
  // ... many other states ...
  someCallback
} = fooState;

// ...

  // If `someCallback` is a method of `fooState`,
  // then `this` in `someCallback` is `undefined`.
  someCallback();

// ...
```