Easier React State Management with OvermindJS

Overmind is a state management library that takes a different approach than Redux. It allows you to keep state manageable without the need to define action types, dispatching, or reducing state.

Nathan Sebhastian
Bits and Pieces

--

Photo by Pixabay from Pexels

State management is one of the hardest things to master in developing web applications with React.

As your project grows in complexity, you need to find a way to manage the state and monitor its changes. Wrong state change can cause unwanted behavior and cause confusion for users.

The most popular solution to manage state is to use Redux, a state management library inspired by Flux design pattern. But state management libraries like Redux has its own problems.

To use it effectively requires you to constantly write boilerplate code in the forms of action types and reducers.

As your project continues to grow bigger, you’ll spend more time helping Redux manage the state correctly by defining actions and reducers rather than actually building your components. This is the problem that leads to the creation of Overmind.

Whatever solution you decide to take on, make sure you never couple your components to the app’s state. It’s becoming more important than ever to keep that in mind as we increasingly make use of component hubs like Bit.dev to publish independent components and reuse them across apps and pages.

Example: exploring React components published on Bit.dev

Reducing the pain of state management

Overmind is a state management library that takes a different approach than Redux. It allows you to keep state manageable without the need to define action types, dispatching, or reducing state.

Overmind does this by containing all your state and actions outside of the application structure, and expose them using a simple Hook API that you can consume in any of your components.

Comparing Redux and Overmind in action

To see how Overmind reduces the pain of managing state, let me show you two identical wizard forms: one built using Redux and the other using Overmind.

First, let’s look at Redux:

The wizard form above is a simple implementation of Redux. First, We define actions as functions that return an object with at least two properties: an action type and the property to change in reducer:

./src/actions/index.jsexport const updateStep = step => ({
type: "STEP",
currentStep: step
});
// ... remaining code omitted for brevity

These actions are then consumed inside the reducer, which will handle state update according to the rules we have defined using action type:

./src/reducer/index.jsconst initialState = {
currentStep: 1,
email: "",
username: "",
password: ""
};
function rootReducer(prevState, action) {
if (typeof prevState === "undefined") {
return initialState;
}
switch (action.type) {
case "STEP":
return Object.assign({}, prevState, {
currentStep: action.currentStep
});
case "EMAIL":
return Object.assign({}, prevState, {
email: action.email
});

case "USERNAME":
return Object.assign({}, prevState, {
username: action.username
});
case "PASSWORD":
return Object.assign({}, prevState, {
password: action.password
});
default:
return prevState;
}
}
export default rootReducer;

Each time an action is called, it will send a “signal” to the reducer to update state through its type. In the case above, an action type of EMAIL will cause the reducer to update email state with the value that we send through action.

A reducer itself is a function where the first argument is injected by Redux. The first argument is the previous state, and the second argument is the action object we’ve called.

Whatever returned by the reducer will become the new current state.

The state is stored in what is known as the Redux Store. You allow your components to access the state by using a two-step process.

First, connect the component with Redux store by using the connect function:

export default connect()(App);

Then, map the state being passed from Redux Store into the component’s props using mapStateToProps function:

const mapStateToProps = state => ({
currentStep: state.currentStep,
email: state.email,
username: state.username,
password: state.password
});
export default connect(mapStateToProps)(App);

With the state being mapped into props, you can use them inside your component:

const App = props => {
// props contain currentStep, email, username, password

Redux Store also passes the dispatch function as a prop into all connected components. By calling this function, we’re letting Redux know that an action function is being triggered and the result needs to be sent to the reducer. We implement dispatch as a wrapper to our action function calls:

./src/App.jsimport {
updateStep,
} from "./actions";
const App = props => {
const _next = () => {
let step = props.currentStep;
props.dispatch(updateStep(step + 1));
};
// ... the rest of the code skipped for brevity
}

Finally, we create a Redux Store that will hold our state:

// ./src/index.jsimport React from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import rootReducer from "./reducers";
const store = createStore(rootReducer);

And pass it into React through the Provider component, which will make the store available to all connected components:

import React from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
import App from "./App";
import rootReducer from "./reducers";
const store = createStore(rootReducer);ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
,
document.getElementById("root")
);

In a nutshell, this is how Redux works. We start off from creating actions, which are processed by the reducer. But notice how we define the initialState inside reducer.

Sometimes I wonder why Redux introduces actions ahead of reducers because defining state should be ahead of defining how the state is changed. Writing a piece of UI component requires a three-step method:

  • Define state
  • Write the logic to update state
  • Consume the new state and update the UI accordingly

By using Redux, we are doing the second step ahead of the first step. It’s somewhat confusing to me.

Furthermore, Redux’s way of confining state change in the reducer has a lot of steps. You need to dispatch actions, define the action function and include its type, move into the reducer and make the switch statement, connect the store to the component, then map the state given by the store into props. That’s a mouthful lot of steps just to do a state change!

Yes, Redux promises that managing state will be predictable, maintainable, and scalable. But it didn’t promise that it will be painless and fun.

How Overmind works

Just like Redux, Overmind tries to bring predictability into your React app by introducing the concept of actions. But that is where the similarity ends because Overmind didn’t use reducers.

Here is the same wizard form, created using Overmind:

First, we start by defining both state and actions inside the createOvermind function call. You can think of this function as the equivalent of Redux createStore, but instead of passing a reducer, you pass a plain object filled with state and actions:

// ./src/overmind/index.jsimport { createOvermind } from "overmind";export const overmind = createOvermind({
state: {
currentStep: 1,
username: "",
email: "",
password: ""
},
actions: {
updateStep({ state }, step) {
state.currentStep = step;
},
updateEmail({ state }, email) {
state.email = email;
},
updateUsername({ state }, username) {
state.username = username;
},
updatePassword({ state }, password) {
state.password = password;
}
}
});

In Overmind, actions are a function with the first parameter injected, much like reducer. But instead of injecting the previous state, Overmind injects the context object, which holds our app’s state, effects, and actions.

Since the code above only needs the state, we simply destructure context and grab its state.

Next, we create a hook by using Overmind’s Hook API to expose context for our components:

import { createOvermind } from "overmind";
import { createHook } from "overmind-react";
export const useOvermind = createHook();export const overmind = createOvermind({
state: {
currentStep: 1,
username: "",
email: "",
password: ""
},
actions: {
updateStep({ state }, step) {
state.currentStep = step;
},
updateEmail({ state }, email) {
state.email = email;
},
updateUsername({ state }, username) {
state.username = username;
},
updatePassword({ state }, password) {
state.password = password;
}
}
});

Now you can use state and actions inside your component:

const App = () => {
const { state, actions } = useOvermind();
const _next = () => {
let step = state.currentStep;
actions.updateStep(step + 1);

};

Finally, just like in Redux, we need to pass the overmind object into Provider component, which will enable components to retrieve state and actions from the Hook API that we’ve created:

// ./src/index.jsimport React from "react";
import ReactDOM from "react-dom";
import { Provider } from "overmind-react";
import { overmind } from "./overmind";

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

And that’s it. In Overmind, there is no action type, no reducer, no switch statement.

Instead of using mapStateToProps and connect, you just call on the Hook API and retrieve the state and actions directly.

As your projects grow in complexity, Overmind will certainly grow with your application, but the way to configure Overmind is exactly how you should develop UI components:

  • You can define state in a separate directory and files
  • You define actions to update state
  • Then you bring up the components

Overmind tries to get out of your way and make state management easier and less painful.

Conclusion

Redux has been a great library that helps us to maintain and develop complex front-end applications. It has been used in many production builds and has proven its capability in creating predictability in our application.

But although it is the most popular state management library, it’s not the only way we can manage state. For one, Redux was made in the days when Hook API wasn’t around and we still have to deal with class components a lot.

If you’re happy with Redux, then by all means continue to use it. But if you feel Redux’s configuration with action types and reducers is too much, then you might be interested to check out Overmind’s documentation. It currently supports React, Vue, and Angular frameworks.

Learn More

--

--

Web Developer and Writer. Sharing what I learn on productivity and success.