Modern State Management with Overmind.js
Martin Anders
Posted on January 27, 2020
Handling state in an application is like juggling with data. This article is about a very interesting way of handling state in your React app with Overmind.js
Why Overmind?
There are many tools for state management out there like Context-API, Redux, MobX or MST. So why we want another one?
In my latest client project I used context based state together with Apollo Client. I'm a huge fan of React Hooks and together with Context-API it's a very nice way to handle state in your application. It seemed to me that I didn't need any bigger state management solution like Redux anymore. In the beginning this was totally fine and I was happy with this approach but after two months the app became bigger and complexity was growing. I wasn't happy anymore and I decided it's time to switch to a different solution.
Back in the days I used and liked Redux a lot and I felt very comfortable with it, but it always had a smell of "too-much-code" for small stuff. On the other hand I never really used MobX but heard only good things about it. After doing some research I found new library called Overmind which locked pretty interesting.
Combining the best together
Overmind was created by Christian Alfoni with the goal to give the best developer experience as possible and also to have strong TypeScript support. The framework internals are hidden to the developer and the API is very simple and straight forward.
- strong TypeScript support
- very simple API
- good documentation and easy to learn
- great devtools
I think one of the best things you get with Overmind is that you have fully typed code nearly for free.
Defining state
Define your state as a simple object. Even if it is Typescript you can define your state as simple like this:
const state = {
// title: string
title: "",
// count: number
count: 0,
// foo: Foo | null
foo: null as Foo | null,
}
HINT: Don't use undefined or ? in your state because it is just considered an anti pattern. Any undefined value on a state will not show up in the devtools, it will not be passed to server or saved in local storage.
Everywhere you use the state you have full TypeScript support and code completion. SIMPLE, right?
Derived state
Derived state are computed values based on other state. In Overmind you define your derived state directly next to the state.
Here we define a computed state variable, doubleCount. It is important to note that the function is memoized by default and runs only when count has changed.
const state = {
count: 0,
// memoized function that only executed when count change
doubleCount: (state) => state.count * 2
}
In Redux you would have to write selectors and use libraries like Reselect to memoize the calculation. But not in Overmind, it's already included. SIMPLE, right?
State mutation
All state mutations are done with actions. Actions have access to your state and can directly change properties.
function incCount(state) {
state.count = state.count + 1
// hint: you get fully typed state and actions here
state.count = "three" // TS Error: Should be number
}
function resetCount(state) {
state.count = 0
}
function setCount(state, value) {
state.count = value
}
HINT: You can't mutate state outside the actions, this means you can't accidentally try to change them in your views.
There is no spread madness like in Redux that comes with immutability. Just change what you want to change. SIMPLE, right?
Side effects
Effects allow you to decouple your app completely from 3rd party APIs. You can read more about them here: overmind effects.
From the docs: Developing applications is not only about managing state, but also managing side effects. A typical side effect would be an HTTP request or talking to localStorage. In Overmind we just call these effects.
Effects should be "initialized" in the onInitialize function of Overmind. There you can provide them with all stuff they need like getters for getting current state or actions to execute.
export const onInitialize = ({ state, effects, actions }) => {
effects.myCoolEffect.initialize({
getSomeState: state.partOfState,
onMoviesLoadSuccess: actions.setMovies
})
}
Access state and actions in components
To get state in a component you have to connect it to Overmind. This is done with the useOvermind hook that provides state and actions. All you have to do is to deconstruct the hook result and you have all you need.
function Counter() => {
// hint: you get fully typed state and actions here
const {
state: { count },
actions: { incCount }
} = useOvermind()
return (
<div>
Count: {count}
<button onClick={incCount}>INC</button>
</div>
)
}
This is all? Yes, it is nuts how easy it is to get state and actions into your components. But wait: How can we prevent the component from rerendering when other parts of the global state have changed, like the title? Our component is only interested in the count property and only wants to rerender if this value changes.
Guess what: Overmind nows exactly which parts of the state the component is using and updates the component only when this part of the state changes. SIMPLE, right?
Mutation tracking
Overmind is using mutation tracking instead of immutability, you can read more about this concept here: immutability vs. mutation tracking.
Christian Alfoni: How you detect the changes and update the UI should just work and be performant! With mutation tracking you put your faith in the system. It relieves you of the mental load of figuring out how components update, it just works and optimally so.
Powerfull devtools
Overmind comes with very powerfull devtools. You can use the VSCode extension or use the standalone version
npx overmind-devtools
You can see all your state and derived state, it's even possible to change it directly inside the tool. You can also see all executed actions with their payload and what part of the state they changed. Sending an action? Sure, you can do this as well.
The view of your app is just an implementation detail. You can write and execute your whole application logic without any views, just with the help of the devtools. This is amazing and SIMPLE, right?
Functional programming style
I'm a big fan of functional programming paradigms, that were introduced with React and became the default in the React world after the release of hooks. Overmind perfectly fits into this. You write only functions, there is no need for classes.
When I had a look in MobX this was a big downside for me because all the examples use classes and I don't want to use classes anymore if possible.
What about pure functions? Mhhh yes sure, Overmind actions are not as pure as regular reducers. In practice it's not a real downside to me because you can test your actions very easy anyway.
Documentation and learning curve
Overmind has a very good documentation. I read it, started to try it in my appliaction and after 3 hours I had refactored my whole app.
The framework internals are hidden to the developer and the api is very simple and straight forward. There is no additional hidden stuff to learn like reselect, redux-sagas, redux-thunk etc.
Cool side note: this was super easy, I was very confident to do the changes because I used react-testing-library. The tests were testing the functionallity of the components without implementation details. I had to do only very little adjustments in test initialization.
After few hours you should already feel very comfortable. Back in the days it took me weeks to wrap my head around Redux. I tried also MobX (while learning Overmind) and it is much harder to read through the docs and to understand all this observer and observable stuff if this is completly new to you.
Running examples
Here you can see a working example on Codesandbox:
Codesandbox Example TypeScript
Summary
I'm so happy I found Overmind, it makes really a lot of fun to use and it simplyfied my app completely. I hope this article can help to convince you to give it a try.
Useful Links
Posted on January 27, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 27, 2024