We at Skillshare embrace change; not because it looks cool in a company’s vision statement, but because it’s necessary. That is the premise behind the recent decision to migrate the whole platform to React, leveraging all the goodness it entails. The group tasked with introducing these changes is but a small delta of our lovely engineering team. Making the right decisions early it’s crucial for getting the rest of the team onboard as smoothly and quickly as possible.

A smooth development experience is everything.

Everything.

And so, along the road of getting React into our codebase, we stumbled upon the most challenging bits of doing frontend development: state management.

Oh boy… were we in for some fun.

Setup

It all started simple: “migrate Skillshare’s header to React.”

“Ah, easy as pie,” we dared saying—it was only the “guest” view which only had a few links and a simple search box. No authentication logic, no session management, nothing magical going on.

Alright, let’s get into some code:

Oh yeah, we use TypeScript—it’s the cleanest, most intuitive, and friendly to all devs. How not to love it? We also use Storybook for UI development, so we’d like to keep components as dumb as possible and do any wiring up at the highest level possible. Since we use Next for server-side rendering, that level would be the page components, which in the end are just plain old components residing in a designated pages folder and automatically mapped to URL requests by the runtime. So, if you have a home.tsx file there, it will be automatically mapped to the /home route—bye bye goes renderToString() .

Alright, that’s it for components… but wait! Getting that search box working also involved setting up a state management strategy, and plain local state wouldn’t get us very far.

Confrontation: Redux

In React, when it comes to state management, Redux is the gold standard—it’s got over 40k stars on GitHub, has full TypeScript support, and big guys like Instagram use it.

Here’s how it works:

Image by @abhayg772

Unlike traditional MVW patterns, Redux manages an application-wide state tree. UI events trigger actions, actions pass data to a reducer, the reducer updates the state tree, and ultimately the UI updates.

Easy, right? Well, let’s do this!

The entity involved here is called a “tag.” Hence, when the user types in the search box, it searches for tags.

Well, that was easy. Now we need some helpers to create our actions dynamically based on input parameters:

There! Now the reducer which updates the state depending of the action:

Whew, that was quite a chunk of code, but now we’re rolling! Remember all wiring goes on at the highest level, and that’d be our page components.

And we’re done! Time to dust off our hands and grab a beer. We have our UI components, a lovely page, and everything nicely glued together.

Uhm… wait.

This is just local data.

We still have to fetch stuff from the actual API. Redux requires actions to be pure functions; they have to be executable right away. And what doesn’t execute right away? Async operations like fetching data from an API. Hence, Redux has to be paired with other libraries to achieve this. There are plenty of options, like thunks, effects, loops, sagas, and each one works differently. That doesn’t only mean additional incline degrees on an already steep learning curve, but even more boilerplate.

And as we trudged along these muddy waters, the obvious echoed over and over in our heads: “all this code just for binding a search box?” And we were sure those would be the exact same words coming from anyone daring enough to venture into our codebase.

One can’t diss Redux; it’s the pioneer in its field and a beautiful concept all around. However, we found it’s way too low-level, demanding you to define everything. It is constantly praised for being very opinionated, preventing you from shooting yourself in the foot by enforcing a pattern, but the price of that is an unholy amount of boilerplate and a thick learning barrier.

That was the dealbreaker for us.

How do we tell our team they won’t be seeing their families during the holidays because of boilerplate?

There’s gotta be something else.

Something more friendly.

Resolution: MobX

At first, we thought of creating some helpers and decorators to circumvent code repetition. That would mean more code to maintain. Also, when core helpers break, or they need new functionality, it can stall the whole team while making changes. You wouldn’t want to lay fingers on a three-year-old helper used by pretty much the whole app, do you?

Then some wild thoughts came along…

“What if we didn’t use redux at all?”

“What else is there?”

A click on that “I’m Feeling Lucky” button yielded the answer: MobX

MobX promises you one thing: to just let you do your work. It applies the principles of reactive programming to React components — yeah, ironically, React is not reactive out of the box. Unlike Redux, you can have multiple stores (i.e. TagsStore , UsersStore , etc.,) or a root store, and bind them to component props. It is there to help you in managing your state, but how you shape it is entirely up to you.

Image by Hanno.co

So we have React integration, full TypeScript support, and minimal-to-no boilerplate.

You know what? I’ll let the code do the talking.

We start by defining our store:

Then wire up the page:

And that’s it… you’re done! We got up and running to the same place we were in the Redux example, except in a matter of a few minutes.

So the code is quite self-explanatory, but to clarify, the inject helper comes from MobX React integration; it’s the counterpart to Redux’s connect helper except that mapStateToProps and mapDispatchToProps are in a single function. The Provider component it’s also MobX, and it takes as many stores as you want which will be later passed on to the inject helper. Also, look at those beautiful, beautiful decorators—that’s how you configure your store. Any property decorated with @observable will notify bound components to re-render on change.

Now that’s what I call “intuitive.”

Need I to say more?

Okay, moving onto API fetching, remember how Redux doesn’t handle async actions out-of -the-box? Remember how you had to use thunks (which are hard to test,) or sagas (which are hard to understand) if you wanted that? Well, with MobX you have plain old classes, so constructor-inject your fetching library of choice and do it in the actions. Miss sagas and generator functions?

Behold the flow helper!

The flow helper takes a generator function which yields steps—response data, logging calls, errors, etc. It’s a series of steps which can be executed gradually or paused if needed.

A flow! Get it?

The times of explaining why sagas are named like that are over. Hell, even generator functions seem less scary now.

Aftermath

Although everything was rainbows and colors so far, an unsettling feeling was still there for some reason—a feeling that going against the current would end up firing back at us. Maybe we needed all that boilerplate to enforce standards. Maybe we needed an opinionated framework. Maybe we needed a well-defined application state tree.

What if we want something like Redux but as convenient as MobX?

Well, for that there is MobX State Tree.

With MST, we define the application state tree using a specialized API, and it is immutable, giving you time traveling, serialization and rehydration, and everything else you can expect from an opinionated state management library.

But enough talk, have at you!

Instead of letting you do whatever you please, MST enforces a pattern by requiring you to define your state tree its way. One might think that this is just MobX but with chained functions instead of classes, but it is way more. The tree is immutable, and each change will create a new “snapshot” of it, enabling time travel, serialization and rehydration, and everything else you felt you were missing.

Addressing the elephant in the room, the only low point is that this is a semi-functional approach to MobX, meaning it ditches classes and decorators, meaning TypeScript support is best effort.

But even so, it’s still pretty great!

Okay, moving on, let’s wire up the page:

See that? Connecting components remains the same, so even the effort of migrating from vanilla MobX to MST is lesser than writing Redux boilerplate.

Why didn’t we go all the way to MST?

Well, MST was overkill for our specific case. We considered it because time travel debugging is a very nice to have, but after stumbling upon Delorean that’s no longer a reason to move over. The day when we need something MobX can’t provide might come, but even falling back to Redux doesn’t seem as daunting thanks to how unobtrusive MobX is.

All in all, we love you, MobX.

Stay awesome.