This article is a spiritual successor to Adding MobX to a vanilla React project, inspired by many of @mwestrase's articles, and comes from a few weeks of refactoring a big Backbone app into a React+MobX app. It might not be the way to structure a MobX app, but it's a way, and it's been working well for me so far.
Design goals for this architecture are:
- minimize boilerplate
- your app is a state machine
- flexibility to move elements
We're going to minimize boilerplate by relying on React contexts and MobX's wonderful inject
function. The combination makes your data stores available everywhere in the app without having to do any wiring and passing by props. Component needs to access the state machine? Inject the store.
Your designer can reshuffle the page as much as he or she wants, and all you have to do is change where components go. No rewiring of the business logic unless the business logic itself changes. For me, this was the missing piece of the puzzle with React. Using this approach, your components become truly independent, and you can do whatever you want.
It's so much fun that I even came to work early once! If you know me, you know how much I like long mornings for myself.
This is how it's done
Contexts and inject give us flexibility and remove boilerplate. What about the state machines?
We're going to treat our MobX stores as the only source of truth in the app and we're going to put actions into them. Actions change state and can be called from anywhere. Putting them in the store reduces boilerplate, makes sure all components have access to them, and lets you see the whole state machine in a single file.
This also makes your app easier to test. If your state machine works, everything works. This is because:
- MobX in strict mode guarantees state only changes in actions
- React guarantees the DOM is a pure expression of state
Yes, MobX works with mutable state and change observers and all that fun stuff. This sounds icky, if you've been listening to the past few years of functional programming propaganda about immutable state.
But you know what's cool? You get all the benefits of that anyway. MobX wraps your state changes in getters and setters, but it could potentially back those with an immutable data structure and actions could build a changelog for a time traveling debugger.
That might be a fun project. I could use a time traveling debugger for MobX ?
Models are another piece of the puzzle.
Models represent specific objects within your overall data structure. MobX stores are for state your whole app cares about; models are for state that only a specific instance of a specific React component cares about.
That sounds like it should go in component state, right? But that’s a bad idea because it makes your stuff harder to test and breaks the state machine ideal.
Models are also a great place to put actions that talk to your backend. Save, fetch, update, that sort of stuff.
Let’s build a box of kakis
You should think of this as pseudocode.
Let's build a box of kakis. I have one of those on my kitchen counter right now because they grow in my girlfriend's mom's backyard. Who knew? ¯*(ツ)*/¯
A box of kakis can be opened or closed. It can be sealed shut with sticky tape, and it contains kakis. Its state machine looks like this:
Once the box is open, you can add or remove as many kakis as you want. You can close the box from any N kaki
state, but you have to open it to add or remove kakis. You can seal it only when it's closed and open it only when it's unsealed.
Box, the model
As a MobX store or model, your box looks like this:
// src/models/Box.js
import { observable, computed, action, extendObservable } from "mobx";
export class Box {
@observable sealed = true;
@observable closed = true;
@observable kakis = [];
constructor(store, initialState) {
this.store = store;
extendObservable(this, initialState);
}
@computed get canSeal() {
return this.closed;
}
@computed get canOpen() {
return !this.sealed;
}
@computed get canManipulatekakis() {
return !this.closed;
}
@action addkaki() {
this.kakis.push(new kaki());
}
@action takekaki() {
this.kakis.pop();
}
@action open() {
if (this.canOpen) {
this.opened = true;
}
}
@action close() {
this.closed = true;
}
@action seal() {
if (this.canSeal) {
this.sealed = true;
}
}
@action unseal() {
this.sealed = false;
}
}
@observable
is a MobX decorator that makes properties observable so MobX can do its thing. extendObservable
is a convenient way to set a bunch of observable values on an object. @computed
marks some properties as deriving purely from state so MobX can memoize them, and @action
marks methods as actions.
Look at the state machine picture. Observables and computeds put together are the circles (states), and actions are the arrows (transitions between states).
Main app component
So much work for a box, right? But look what happens when we put this in a React component:
// src/components/App.js
import { Provider } from "mobx-react";
import { Box } from "./models/Box";
// would normally go in src/stores/...
class MainStore {
@observable box = null;
@action getBoxFromMail() {
this.box = new Box({
sealed: true,
closed: true,
kakis: [new kaki(), new kaki()],
});
}
}
class App extends Component {
mainStore = new MainStore();
render() {
const mainStore = this.mainStore;
return (
<provider mainstore={mainStore}>
<div>
<button onclick={mainStore.getBoxFromMail.bind(mainStore)}>
Get Mail
</button>
<box></box>
</div>
</provider>
);
}
}
We set up a new MainStore
, which holds the main state for our app. Stuff that everyone needs like have you picked up the box of kakis from the mail yet, is your counter black, is your refrigerator running. That sort of thing.
Inside the render function, we use Provider
to add mainStore
to a React context. Any component inside Provider
can get access to mainStore
if it needs to.
The nice thing about bundling actions with your stores and models is that you can use them in onClick
handlers. Which means most of your components can be stateless functional components.
Box, the React component
Let me show you.
// src/components/Box.js
import { inject, observer } from 'mobx-react';
const SealedOrOpened = observer(({ box }) => (
if (box.sealed) {
return 'Sealed and Closed';
}else if (!box.sealed && !box.opened) {
return 'You should open the box';
}else{
return 'Take some kakis!';
}
));
// this helper normally goes somewhere else
const If = ({ cond, children }) => cond ? children : null;
const Box = inject('mainStore')(observer(({ mainStore }) => {
const box = mainStore.box;
return (
<div>
<sealedoropened box={box}>
<if cond={box.canOpen}>
<button onclick={box.open.bind(box)}>Open box</button>
</if>
<if cond={box.opened}>
<ul>
{box.kakis.map(kaki => <kaki>)}
</kaki></ul>
<button onclick={box.takekaki.bind(box)}>
Take a kaki
</button>
</if>
</sealedoropened></div>
);
));
That's the Box
component in spirit. It renders some text telling you what to do with the box and gives you a button that opens it. Yes, I know there should be a button to take off the sticky tape, too.
Once you open the box, a list of kakis appears and a button to take them out one by one.
Notice that Box
is a functional stateless component. You can do that because all the state and all the actions are in the Box
MobX model. There’s no need to build local click handlers.
You might also notice that we inject('mainStore')
'd into the Box component. That lets us move the component to anywhere within the DOM tree inside Provider
and it will continue to work. No changes necessary.
❤️ all around
Your designer will love the flexibility, and your PM will fart rainbows at how fast you are.
Oh, and that observer
thingy? That ensures your components re-render when state changes.
Next week, I'm going to write about persisting those changes to a backend of some sort. I’m still working on how to make that part effortless and clean :)
Maybe I should make a Building Webapps With React and MobX book ? Tell me if you're interested.
Continue reading about How to structure your MobX app for the real world
Semantically similar articles hand-picked by GPT-4
- Livecoding #25: Adding MobX to a vanilla React project
- Simple MobX-driven modals
- One awkward thing about MobX: Complex models
- Backbone → React: it's a people problem after all ?
- useReducer + useContext for easy global state without libraries
Learned something new?
Read more Software Engineering Lessons from Production
I write articles with real insight into the career and skills of a modern software engineer. "Raw and honest from the heart!" as one reader described them. Fueled by lessons learned over 20 years of building production code for side-projects, small businesses, and hyper growth startups. Both successful and not.
Subscribe below 👇
Software Engineering Lessons from Production
Join Swizec's Newsletter and get insightful emails 💌 on mindsets, tactics, and technical skills for your career. Real lessons from building production software. No bullshit.
"Man, love your simple writing! Yours is the only newsletter I open and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. 👌"
Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.
Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.
Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.
Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev
Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization
Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections
Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog
Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com
By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️