Example App: Clean Architecture with React, Overmind and Local Storage, pt. 1
Tags: react, overmind, cleanarchitecture, productivity
Published: 2022-01-30 03:56:31

Disclaimer: This article may seem to bounce around like a squirrel in an acorn field before the winter.

TL;DR: [GitHub repository](https://github.com/sutt0n/clean-architecture-monorepo).

So, clean architecture! I'm a huge proponent of it. It's a great way to ensure that your testability of your project is so easy, a monkey could do it. What is clean architecture? It's something that pushes us to completely separate the business logic from the tech stack by allowing us to define clear boundaries by using dependency injection (we'll do this via an `applicationContext`):

![Clean Architecture Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yp7txnj04kxt4vxa9kcg.png)

I'm not going to go into detail, because clean architecture is a principle best explained by others; for example, this [summary on Gist](https://gist.github.com/ygrenzinger/14812a56b9221c9feca0b3621518635b). Who founded this concept? Uncle Bob Martin. You can check out his [Twitter](https://twitter.com/unclebobmartin)!

## How 

How are we going to implement this separation? We'll be implementing everything in a monorepo via Lerna. I _was_ going to utilize Terraform with this, but decided that was borderline over-engineering something simple like an Example Application. Maybe in the future!

## Structure

How's the packaging going to look? What about the file structure? First off, we'll need a view, `ui` - that'll be our frontend in which I used `create-react-app` with a custom [template](https://github.com/sutt0n/cra-template-overmind) I created.

Secondly, we'll need a place for our business logic, `business`. This will hold our entities, use-cases, and such. Thirdly, we'll need a place for storage methods, `persistence`. This is where methods for local storage will live.

Here's what our structure looks like so far:

- `packages/ui`
- `packages/business`
- `packages/persistence`


# The View

Let's dive in. So, I stated that I have a `create-react-app` template. This is essentially my frontend boilerplate for clean architecture that I've made - it's just  for frontend and local storage. For TypeScript aficionados, I will be making one shortly after this article. The template has everything wrapped around the frontend, including local storage for persistence; however, I moved things around for this article.

## Overmind

I used (https://overmindjs.org/)[Overmind] for state management. It's a more declarative state management system, and allows you to be as complex as you want. It's heavily aimed at allowing the developer to focus on the testability and readability of his/her application. 

I'll be writing an article on Overmind as well. 😁

## Code

Okay, we're actually diving in now. I promise.

First off, we have our plain ole' `index.js` which pulls in Overmind to the UI:

```jsx
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import { createOvermind } from 'overmind';
import { Provider } from 'overmind-react';
import { config } from './presenter/presenter';
import App from './App.jsx';

const overmind = createOvermind(config);

ReactDOM.render(
  <Provider value={overmind}>
    <App />
  </Provider>,
  document.getElementById('root'),
);
```

That's easy enough. I won't post `App.jsx`, but it's just going to just reference the `Todos` component in `views/Todos.jsx`: 

```jsx
import * as React from 'react';
import { useActions, useState } from '../presenter/presenter';

const Todo = ({ todo }) => {
  useState();
  return (
    <li>
      {todo.title} {todo.description}
    </li>
  );
};

export const Todos = () => {
  const state = useState();
  const {
    addTodoItemAction,
    updateTodoTitleAction,
    updateTodoDescriptionAction,
  } = useActions();

  return (
    <>
      <input
        type="text"
        name="title"
        placeholder="Title"
        onChange={e => updateTodoTitleAction(e.target.value)}
      />
      <input
        type="textarea"
        name="description"
        placeholder="Description"
        onChange={e => updateTodoDescriptionAction(e.target.value)}
      />
      <button onClick={addTodoItemAction}>Add</button>
      <ul>
        {state.todos.map(todo => (
          <Todo key={todo.id} todo={todo} />
        ))}
      </ul>
    </>
  );
};
```

Since we're diving into Overmind territory, I'll explain some things we have going on here: we have two hooks, `useActions` and `useState` which pulls in our current state of the application and Overmind [actions](https://overmindjs.org/core/writing-application-logic). Actions are essentially where state reads and mutations happen, and it's where we inject our `applicationContext`. I've named the directory where Overmind lives as `presenter`, because that's where our presentation logic will live. 

Let's look at that file, `ui/presenter/presenter.js`:

```js
import {
  createStateHook,
  createActionsHook,
  createEffectsHook,
  createReactionHook,
} from "overmind-react";
import { state } from "./state";
import { applicationContext } from '../applicationContext';
import { addTodoItemAction } from './actions/addTodoItemAction';
import { updateTodoTitleAction } from './actions/updateTodoTitleAction';
import { updateTodoDescriptionAction } from './actions/updateTodoDescriptionAction';
import { deleteTodoItemAction } from './actions/deleteTodoItemAction';

const actions = {
  addTodoItemAction,
  updateTodoTitleAction,
  updateTodoDescriptionAction,
  deleteTodoItemAction,
};

export const config = {
  state,
  actions,
  effects: applicationContext,
};

export const useState = createStateHook();
export const useActions = createActionsHook();
export const useEffects = createEffectsHook();
export const useReaction = createReactionHook();
```

After gandering at that, you're probably anxious to see what an action looks like with an `applicationContext`. Before I show y'all `applicationContext`, let's gander at the `presenter/actions/addTodoItemAction.js`:

```js
export const addTodoItemAction = ({ state, effects: { ...applicationContext }}) => {
  const { todoTitle: title, todoDescription: description } = state;

  const todos = applicationContext.getUseCases().addTodoItemInteractor({
    applicationContext,
    title,
    description,
  });
  
  state.todos = todos;
}
```

Pretty simple (it gets simpler for those that whom are confused, I promise), really. We grab our use cases from `applicationContext`. You may be asking, "Why not just include the interactor? Why go through that? Well, let's look at the unit test:

```js
const { createOvermindMock } = require("overmind");
const { config } = require("../presenter");

describe("addTodoItemAction", () => {
  let overmind;
  let addTodoItemInteractorStub;
  let mockTodo = { title: "TODO Title", description: "TODO Description" };

  beforeEach(() => {
    addTodoItemInteractorStub = jest.fn().mockReturnValue([mockTodo]);

    // TODO: refactor
    overmind = createOvermindMock(
      {
        ...config,
        state: { todoTitle: "TODO Title", todoDescription: "TODO Description" },
      },
      {
        getUseCases: () => ({
          addTodoItemInteractor: addTodoItemInteractorStub,
        }),
      }
    );
  });

  it("calls the interactor to add a todo item", async () => {
    await overmind.actions.addTodoItemAction();

    expect(addTodoItemInteractorStub).toHaveBeenCalled();
    expect(addTodoItemInteractorStub).toHaveBeenCalledWith({
      applicationContext: expect.anything(),
      ...mockTodo,
    });
    expect(overmind.state).toEqual(
      expect.objectContaining({
        todos: [mockTodo],
      })
    );
  });
});
```

I'd much rather mock out `applicationContext` than use `jest.mock` for each test. Having a context that unit tests can share for a potentially large codebase will save us a lot of time in writing these tests out. Another reason I _believe_ it's better is for designing/defining your logic via Test Driven Development.

## Business

Well, we've covered the actions that call our use-cases, or `interactors`. Let's dive into our business logic by first taking a look at the interactor being called from our action above, `packages/business/useCases/addTodoItemInteractor.js`:

```js
import { Todo } from '../entities/Todo';

/**
 * use-case for adding a todo item to persistence
 *
 * @param {object} provider provider object
 */
export const addTodoItemInteractor = ({ applicationContext, title, description }) => {
  const todo = new Todo({ title, description }).validate().toRawObject();
  
  const todos = [];
  const currentTodos = applicationContext.getPersistence().getItem({
    key: 'todos',
    defaultValue: [],
  });

  if (currentTodos) {
    todos.push(...currentTodos);
  }

  todos.push(todo);

  applicationContext.getPersistence().setItem({ key: 'todos', value: todos });

  return todos;
};
```

Do you see where we're going with this? This interactor is the use-case surrounding the entity, `Todo` in the diagram above. It calls two `persistence` methods, which are just essentially local storage wrappers I've created. Let's take a gander at the unit test for this interactor:

```js
const { addTodoItemInteractor } = require("./addTodoItemInteractor");

describe("addTodoItemInteractor", () => {
  let applicationContext;
  let getItemStub;
  let setItemStub;

  beforeAll(() => {
    getItemStub = jest.fn().mockReturnValue([]);
    setItemStub = jest.fn();

    applicationContext = {
      getPersistence: () => ({
        getItem: getItemStub,
        setItem: setItemStub,
      }),
    };
  });

  it("add a todo item into persistence", () => {
    const result = addTodoItemInteractor({
      applicationContext,
      title: "TODO Title",
      description: "TODO Description",
    });

    expect(getItemStub).toHaveBeenCalled();
    expect(getItemStub).toHaveBeenCalledWith({
      key: "todos",
      defaultValue: [],
    });
    expect(setItemStub).toHaveBeenCalled();
    expect(setItemStub).toHaveBeenCalledWith({
      key: "todos",
      value: [
        {
          title: "TODO Title",
          description: "TODO Description",
        },
      ],
    });
    expect(result).toEqual([
      {
        title: "TODO Title",
        description: "TODO Description",
      },
    ]);
  });
});
```

Easy, breezy, beautiful. Everything can be stubbed, mocked out, etc. All we care about is the primitive logic within the interactor itself - not what's in local storage, not what persistence we're using whether it's local storage or a remote/local database, and we don't care about the UI or any of the Overmind logic. 

All we care about is the business logic. That's all we're testing here, and that's all we _care_ about testing here. Let's take a look at those persistence methods, `setItem` and `getItem`.

## Persistence

The two methods called above are `setItem` and `getItem`. Pretty straight-forward. Honestly, I probably didn't have to wrap them; however, I wanted to show that for persistence to be able to be easily interchangeable no matter what we use, practically nothing has to change inside the interactor.

Let's look at `setItem`:

```js
module.exports.setItem = ({ key, value }) =>
  localStorage.setItem(key, JSON.stringify(value));
```

Easy enough. The unit test:

```js
const { setItem } = require('./setItem');

describe('setItem', () => {
  let setItemStub;
  global.localStorage = {};

  beforeEach(() => {
    setItemStub = jest.fn();

    global.localStorage.setItem = setItemStub;
  });

  it('sets the item given the key/value pair', () => {
    setItem({ key: 'todos', value: 'todos value' });

    expect(setItemStub).toHaveBeenCalled();
    expect(setItemStub).toHaveBeenCalledWith('todos', JSON.stringify('todos value'));
  });
});
```

Simple enough, right? There's a pattern to unit tests and I'm sure with some ideas, one could find a way to reduce boilerplate... or just make a macro since most everything repeated is essential to its respective unit test. 

Note: The only reason why we're stringifying with JSON is we're allowing the storage of objects/arrays (if you noticed in the action, the `todos` are an array).

That's obviously not everything. I didn't want to dive too deep into the specifics. My next article will include us hooking this same setup with a backend (more than likely serverless). What database should we use? DynamoDB or a relational database like PostgreSQL? Maybe both?

Thanks for reading! If I typed something wrong or if you have any questions, comments, concerns, or suggestions then please post them in a comment! Y'all take care now.