Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
January 30, 2022 03:56 am GMT

Example App: Clean Architecture with React, Overmind and Local Storage

We're not going to get into why I haven't been writing - we're just gonna dive right into it.

TL;DR: GitHub repository.

So, clean architecture! I'm a huge proponent of it. It's a great way to ensure 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

I'm not going to go into detail, because clean architecture is a principle best explained others; for example, this summary on Gist. Who founded this concept? Uncle Bob Martin. You can check out his Twitter!

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

So, 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 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 only for frontend and local storage. For TypeScript aficionados, I will be making one shortly after this article. The template has everything wrapped around frontend, including local storage for persistence; however, I moved things around for this article.

Overmind

I used 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 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:

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:

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. 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:

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:

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:

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:

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:

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:

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

Easy enough. The unit test:

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.


Original Link: https://dev.to/sutt0n/example-app-clean-architecture-with-react-overmind-and-local-storage-59ci

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To