Achieving a Cleaner State in your React App with Overmind (Basic)

sutt0n

Joseph Sutton

Posted on February 5, 2022

Achieving a Cleaner State in your React App with Overmind (Basic)

Today's mainly going to be focused on the frontend, because I want to introduce this state management system that I've been really digging lately. It's called Overmind, the same team that made Cerebral. Overmind is somewhat similar to Cerebral, but it supports TypeScript and it's... well, it's not abandoned.

TLDR: GitHub Repository.

Really, Another Daggum State Management System?

Yep. Like all the others say, "bUt tHiS ONe iS DiFfErEnT!" It honestly is - Overmind is a more declarative approach to state management orchestration. You give it a state structure, you tell it how the state is mutated and when the state is mutated, and you'll be a happier developer for it.

Okay, Fine

See? I knew you'd come around! Alright, let's get our boots on with React using TypeScript:

npx create-react-app overmind-shenanigans --template typescript

Now, let's add Overmind to our React project:

npm install overmind overmind-react

Cool, we're done! Just kidding - we need to configure it first in src/presenter/index.ts:

import { createStateHook, createActionsHook } from 'overmind-react';
import { state } from './state';
import * as actions from './actions';
import { IContext } from 'overmind';

export const config = {
  state,
  actions,
};

export type Context = IContext<{
  state: typeof config.state;
  actions: typeof config.actions;
}>;

export const useAppState = createStateHook<Context>();
export const useActions = createActionsHook<Context>();
Enter fullscreen mode Exit fullscreen mode

Note that we're missing a few files, the state and actions files - don't worry, we'll get to those. Since we have our configuration defined, let's go ahead and hook it into our React app in index.tsx:

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

const overmind = createOvermind(config);

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

Cool. Let's start doing things. First, let's add some good ole' fashioned TODO functionality. We'll use a combination of the component's state (temporary storage for the todo title and description), local storage, and the state managed by Overmind.

State

Let's set up our state structure in src/presenter/state.ts:

export type Todo = {
  title: string,
  description: string,
};

export const state = {
  todos: [] as Todo[],
};
Enter fullscreen mode Exit fullscreen mode

Action

Let's write our action in src/presenter/actions/addTodoAction.ts:

import type { Context } from "../";
import { Todo } from "../state";

export const addTodoAction = (
  { state }: Context,
  { title, description }: Todo
) => {
  state.todos.push({
    title,
    description,
  });
};
Enter fullscreen mode Exit fullscreen mode

For encapsulation's sake (and our config above), let's create our src/presenter/actions.ts file:

import { addTodoAction } from "./actions/addTodoAction";

export { addTodoAction };
Enter fullscreen mode Exit fullscreen mode

Creating our TODO

Nothing special here, pretty simple. This isn't an article about CSS, it's about Overmind. Let's create the components that both add TODOs and list them. First, adding our TODOs with src/components/Todo.tsx:

import React, { useState } from "react";
import { useActions } from "../presenter";

export const Todo = () => {
  const [title, setTitle] = useState<string>('');
  const [description, setDescription] = useState<string>('');

  const actions = useActions();

  return (
    <>
      <div>
        <input
          name="title"
          type="text"
          value={title}
          placeholder="Title"
          onChange={(e) => setTitle(e.target.value)}
        />
        <input
          name="description"
          type="text"
          value={description}
          placeholder="Description"
          onChange={(e) => setDescription(e.target.value)}
        />
      </div>
      <div>
        <button onClick={() => {
          actions.addTodoAction({ title, description })
        }}>Add Todo</button>
      </div>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

Notice how we pull in our actions, and call addTodoAction. You can most definitely implement some validation here, too! Now, listing our TODOs with src/components/Todos.tsx:

import React from "react";
import {  useAppState } from "../presenter";

export const Todos = () => {
  const state = useAppState();

  return (
    <>
      {state.todos.map(todo => (
        <ul key={`todo-title-${todo.title}`}>
          <li><b>{todo.title}</b> - {todo.description}</li>
        </ul>
      ))}
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

Let's put those two components in our src/App.tsx file:

import React from 'react';
import './App.css';
import { Todo } from './components/Todo';
import { Todos } from './components/Todos';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <Todo />
        <Todos />
      </header>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

You'll notice when we refresh the page, things don't persist. If you're normally a React developer, you'll know they won't even before refreshing. Let's talk about persisting our TODOs from the state to local storage with an effect.

Effects

Overmind effects are exactly what you think they are: side effects. You can do anything, from slapping axios to an SQLite library in there. With ours, we're going to just add an effect that accesses local storage.

With that, let's add our setItem effect in src/presenter/effects/setItem.ts:

import { Todo } from "../state";

export const setItem = (key : string, item : Todo) => {
  localStorage.setItem(key, JSON.stringify(item));
}
Enter fullscreen mode Exit fullscreen mode

Now, our src/presenter/effects/getItem.ts:

export const getItem = (key : string) => {
  const item = localStorage.getItem(key);

  if(item) {
    return JSON.parse(item);
  }

  return null;
}
Enter fullscreen mode Exit fullscreen mode

And our encapsulation in src/presenter/effects.ts:

import { getItem } from './effects/getItem';
import { setItem } from './effects/setItem';

export { getItem, setItem };
Enter fullscreen mode Exit fullscreen mode

This will change our config and state context type. Let's go ahead and update that to our config in src/presenter/index.ts:

import { createStateHook, createActionsHook } from 'overmind-react';
import { state } from './state';
import * as actions from './actions';
import { IContext } from 'overmind';
import * as effects from './effects'

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

export type Context = IContext<{
  state: typeof config.state;
  actions: typeof config.actions;
  effects: typeof config.effects;
}>;

export const useAppState = createStateHook<Context>();
export const useActions = createActionsHook<Context>();
Enter fullscreen mode Exit fullscreen mode

Now that's updated, we need to do a few things. First, we need to add the effect usage to local storage in our action, src/presenter/actions/addTodoItem.ts:

import type { Context } from "../";
import { Todo } from "../state";

export const addTodoAction = (
  { state, effects }: Context,
  { title, description }: Todo
) => {
  const currentTodos = effects.getItem('todos') || [];

  const newTodo = {
    title, description,
  };

  currentTodos.push(newTodo);

  state.todos = currentTodos;

  effects.setItem('todos', currentTodos);
};
Enter fullscreen mode Exit fullscreen mode

Now, let's try it out. Add some TODOs, and refresh the page. You'll notice that it's still not showing our persisted TODOs in our local storage and that's because we need to initialize the state from local storage with the persisted TODOs. Thankfully, Overmind allows us to do that with an initialization action.

Let's create that initialization action in src/presenter/actions/onInitializationOvermind.ts:

import type { Context } from "../";

export const onInitializeOvermind = (
  { state, effects }: Context
) => {
  const currentTodos = effects.getItem('todos') || [];

  state.todos = currentTodos;
};
Enter fullscreen mode Exit fullscreen mode

Let's add it to our src/presenter/actions.ts:

import { addTodoAction } from "./actions/addTodoAction";
import { onInitializeOvermind } from "./actions/onInitializeOvermind";

export { addTodoAction, onInitializeOvermind };
Enter fullscreen mode Exit fullscreen mode

Now you can refresh the page and it should load any persisted TODOs.

I'll have an article written up on a full-stack application using Overmind with multiple models soon. It'll include the clean architecture that I previously wrote about.

There are some pros and cons to this state management system, as there are with any others. There's a lot of advanced addons / built-in functionality that allows the developer to control the state and how it flows / mutates. For example, Overmind has state machines as well (similar to XState).

However, the best parts I like about Overmind is the encapsulation and testability. If you go over to this article's repository, you'll notice that every effect and action is unit tested.

Thank y'all for reading! My next article will be either a soft skills type of article, or that full-stack clean architecture article that extends on the previous one.

💖 💪 🙅 🚩
sutt0n
Joseph Sutton

Posted on February 5, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related