Achieving a Cleaner State in your React App with Overmind (Basic)
Joseph Sutton
Posted on February 5, 2022
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>();
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')
);
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[],
};
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,
});
};
For encapsulation's sake (and our config above), let's create our src/presenter/actions.ts
file:
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
:
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
:
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:
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
:
import { Todo } from "../state";
export const setItem = (key : string, item : Todo) => {
localStorage.setItem(key, JSON.stringify(item));
}
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;
}
And our encapsulation in src/presenter/effects.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
:
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
:
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
:
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
:
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.
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.
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
July 27, 2024