Achieving a Cleaner State in your React App with Overmind (Basic)
Tags: react, typescript, tutorial
Published: 2022-02-05 17:01:34

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](https://overmindjs.org/), the same team that made [Cerebral](https://cerebraljs.com/). Overmind is somewhat similar to Cerebral, but it supports TypeScript and it's... well, it's not abandoned. 

TLDR: [GitHub Repository](https://github.com/sutt0n/overmind-shenanigans).

### 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`:

```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>();
```

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

```ts
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')
);
```

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

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

export const state = {
  todos: [] as Todo[],
};
```

## Action

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

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

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

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

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

export { addTodoAction };
```

## 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`:

```ts
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>
    </>
  );
};
```

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

```ts
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>
      ))}
    </>
  );
};
```

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

```ts
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;
```

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

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

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

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

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

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

  return null;
}
```

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

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

export { getItem, setItem };
```

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

```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>();
```

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

```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);
};
```

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

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

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

  state.todos = currentTodos;
};
```

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

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

export { addTodoAction, onInitializeOvermind };
```

_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](https://dev.to/sutt0n/example-app-clean-architecture-with-react-overmind-and-local-storage-59ci).

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](https://overmindjs.org/guides-1/using-state-machines) as well (similar to [XState](https://xstate.js.org/)).

However, the best parts I like about Overmind is the encapsulation and testability. If you go over to this article's [repository](https://github.com/sutt0n/overmind-shenanigans), 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](https://dev.to/sutt0n/example-app-clean-architecture-with-react-overmind-and-local-storage-59ci).