• How We Ditched Redux for MobX
  • Luis Aguilar
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: lihaobhsfer
  • Proofreader: Power car

How did we get rid of Redux in favor of MobX

At Skillshare we embrace change; Not just because it was cool to write about in the company’s vision statement, but because change was necessary. This was the premise behind our recent decision to migrate our entire platform to React and take advantage of all of its advantages. The team that performed this task was just a small part of our engineering team. Making the right decision as early as possible is critical to allowing the rest of the team to switch platforms as quickly and smoothly as possible.

A smooth development experience is everything.

Everything.

Then, when introducing React into our code base, we encountered one of the most challenging parts of front-end development: state management.

Alas… Now it gets interesting.

Set up the

It all started with a simple task: “Migrate Skillshare’s header to React.”

“A piece of cake! We set the flag — the header is just a visitor view, with a few links and a simple search box. There is no authorization logic, no session management, nothing particularly magical.

Ok, let’s do some code:

interface HeaderProps { searchBoxProps: SearchBoxProps; } class Header extends Component<HeaderProps> { render() { return ( <div> <SearchBox {... this.props.searchBoxProps} /> </div> ); } } interface SearchBoxProps { query? : string; isLoading: string; data: string[]; onSearch? : (query: string) => void; } class SearchBox extends Component<SearchBoxProps> {render() {// Render... }}Copy the code

Yes, we use TypeScript — it’s the cleanest, most intuitive, and most developer-friendly language out there. How could not love it? We also use Storybook for UI development, so we want to make the components as dumb as possible and splice them together at as high a level as possible. Since we did server-side rendering with Next, that hierarchy was page components, which ended up being just well-known components in the designated Pages directory that were automatically mapped to URL requests at run time. So, if you have a home.tsx file, it’s automatically mapped to the /home route — say goodbye to renderToString().

Ok, so much for components… But wait a minute! Implementing the search box also requires a state management strategy, and local state won’t get us very far.

Against: Redux

In React, Redux is the golden rule when it comes to state management — it has 4W + Star on Github (as of the time of publication in English). 5W + Star as of the release of this translation. , fully supports TypeScript and is used by big companies like Instagram.

Here’s how it works:

Unlike the traditional MVW style, Redux manages a state tree that covers the entire application. The UI triggers Actions, which pass data to the Reducer, which updates the state tree and ultimately updates the UI.

Pretty simple, right? More code!

The entities involved here are tags. Therefore, when the user types in the search box, the search box searches for the label

/* There are three actions: * - Tag search: when the user enters, a new search is triggered. * - Tag search updates: When the search results are ready, they must be updated. * - Tag search error: something bad happened. * /

enum TagActions {
    Search = 'TAGS_SEARCH',
    SearchUpdate = 'TAGS_SEARCH_UPDATE',
    SearchError = 'TAGS_SEARCH_ERROR',}interface TagsSearchAction extends Action {
    type: TagActions.Search;
    query: string;
}

interface TagsSearchUpdateAction extends Action {
    type: TagActions.SearchUpdate;
    results: string[];
}

interface TagsSearchErrorAction extends Action {
    type: TagActions.Search;
    err: any;
}

type TagsSearchActions = TagsSearchAction | TagsSearchUpdateAction | TagsSearchErrorAction;
Copy the code

It’s pretty easy. Now we need some helper functions to dynamically create actions based on the input parameters:

const search: ActionCreator<TagsSearchAction> =
    (query: string) = > ({
        type: TagActions.Search,
        query,
    });

const searchUpdate: ActionCreator<TagsSearchUpdateAction> =
    (results: string[]) = > ({
        type: TagActions.SearchUpdate,
        results,
    });

const searchError: ActionCreator<TagsSearchErrorAction> =
    (err: any) = > ({
        type: TagActions.SearchError,
        err,
    });

Copy the code

Done! Next is the reducer responsible for updating state based on action:

interface State {
    query: string;
    isLoading: boolean;
    results: string[];
}

const initialState: State = {
    query: ' ',
    isLoading: false,
    results: [],
};

const tagSearchReducer: Reducer<State> =
    (state: State = initialState, action: TagsSearchActions) = > {
        switch ((action as TagsSearchActions).type) {
            case TagActions.Search:
                return {
                    ...state,
                    isLoading: true,
                    query: (action as TagsSearchAction).query,
                };

            case TagActions.SearchUpdate:
                return {
                    ...state,
                    isLoading: false,
                    results: (action as TagsSearchUpdateAction).tags,
                };

            case TagActions.SearchError:
                return {
                    ...state,
                    isLoading: false,
                    results: (action as TagsSearchErrorAction).err,
                };

            default:
                returnstate; }};Copy the code

This code is quite long, but we are making progress! All the stitching happens at the top level, our page component.

interface HomePageProps { headerProps? : HeaderProps; } class HomePage extends Component<IndexPageProps> { render() { return ( <Header {... this.props.headerProps} /> <! -- the rest of the page .. -- - >); } } const mapStateToProps = (state: State) => ({ headerProps: { searchBoxProps: { isLoading: state.isLoading, results: state.results, query: state.query, } } }); const mapDispatchToProps = (dispatch: Dispatch) => ({ headerProps: { searchBoxProps: { onSearch: (query: string) => dispatch(TagActions.search(query)), } } }); const connectedPage = connect(mapStateToProps, mapDispatchToProps)(HomePage); const reducers = combineReducers({ tagSearch: tagSearchReducer, }); const makeStore = (initialState: State) => { return createStore(reducers, initialState); } // It's all coming together here -- see: the home page! export default withRedux(makeStore)(connectedPage);Copy the code

Mission accomplished! Dust off your hands and have a beer. We’ve got UI components, a page, all nicely connected together.

Emmm… Wait a minute.

This is just the local state.

We still need to get the data from the real API. Redux requires actions to be pure; They must be immediately executable. What will not be executed immediately? Asynchronous operations such as retrieving data from the API. Therefore, Redux must work with other libraries to achieve this functionality. There are several libraries to choose from, such as Thunks, Effects, Loops, sagas, all of which are somewhat different. This doesn’t just mean adding another slope to an already steep learning curve, it means more templates.

As we waded through the mud, the obvious question kept echoing in our minds: All those lines of code to bind a search box? We are sure that anyone brave enough to look at our code base will ask the same question.

We can’t Diss Redux; It is a pioneer in the field and an elegant concept. However, we found it too “low-level” and required you to define everything yourself. It has been praised for having very clear ideas that keep you from shooting yourself in the foot when trying to force a style, but all this comes at a cost in the form of a lot of template code and a huge learning disability.

We can’t put up with that.

How do we have the heart to tell our team that they’re coming in over the holidays because of template code?

There’s got to be something else.

More friendly tools.

You don’t necessarily need Redux

Solution: MobX

Initially, we thought about creating helper functions and decorators to solve code duplication. And that means more code to maintain. Also, when the core helper functions break down or new functionality is required, changing them can force the entire team to stop working. You don’t want to touch the help function code you wrote three years ago and use throughout your application, do you?

Then we had a bold idea…

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

“What else can I do?”

Click on the “I’m feeling lucky today” button and we get the answer: MobX

MobX guarantees one thing: you do your job. It applies the principles of responsive programming to the React component — and, ironically, React isn’t exactly responsive out of the box. Unlike Redux, you can have multiple stores (such as TagsStore, UsersStore, and so on), or a total store, and bind them to the props of the component. It helps you manage your state, but how you structure it is up to you.

So we now integrate React, full TypeScript support, and minimal templates.

Let the code speak for itself.

We define store first:

import { observable, action, extendObservable } from 'mobx';

export class TagsStore {
    private static defaultState: any = {
        query: ' ',
        isLoading: false,
        results: [],
    };

    @observable public results: string[];

    @observable public isLoading: boolean;

    @observable public query: string;

    constructor(initialState: any) {
        extendObservable(this, {... defaultState, ... initialState}); }@action public loadTags = (query: string) = > {
        this.query = query;

        // Some business code...}}export interface StoreMap {
    tags: TagsStore,
}
Copy the code

Then splice the page:

import React, { Component } from 'react'; import { inject, Provider } from 'mobx-react'; import { Header, HeaderProps } from './header'; export interface HomePageProps { headerProps? : HeaderProps; } export class HomePage extends Component<IndexPageProps> { render() { return ( <Header {... this.props.headerProps} /> <! -- the rest of the page .. -- - >); } } export interface StoreMap { tags: TagsStore; } export const ConnectedHomePage = inject(({ tags }: StoreMap) => ({ headerProps: { searchBoxProps: { query: tags.query, isLoading: tags.isLoading, data: tags.data, onSearch: tags.loadTags, } } })); export const tagsStore = new TagsStore(); export default () => { return ( <Provider tags={tagsStore}> <ConnectedHomePage> </Provider> ); }Copy the code

And that’s it! We’ve implemented all of the functionality in the Redux example, but this time in just a few minutes.

The code is fairly clear, but to clarify the inject help function comes from MobX React; It is contrasted with Redux’s Connect helper function, except that its mapStateToProps and mapDispatchToProps are in one function. The Provider component also comes from MobX and can put any number of stores in it, which will be passed into the Inject help function. And look at all those charming, charming decorators — that’s how you configure the Store. All entities decorated with @Observable tell the bound component to rerender if it changes.

That’s intuitive.

What more needs to be said?

Then, regarding access to the API, remember that Redux cannot handle asynchronous operations directly? Remember when you had to use Thunks (which were very hard to test) or SAGAs (which were very hard to understand) to implement asynchronous operations? So, with MobX, you can use ordinary classes, inject the API access library of your choice into the constructor, and then execute it in the Action. Still missing Sagas and Generator functions?

Here it is, the flow helper function!

import { action, flow } from 'mobx';

export class TagsStore {

    // ..

    @action public loadTags = flow(function * (query: string) {
        this.query = query;
        this.isLoading = true;

        try {
            const tags = yield fetch('http://somewhere.com/api/tags');
            this.tags = tags;
        } catch (err) {
            this.err = err;
        }

        this.isLoading = false; })}Copy the code

The flow helper function uses generator functions to generate steps — responding to data, logging calls, reporting errors, and so on. It is a series of steps that can be performed incrementally or paused as needed.

A process! Don’t understand?

The time to explain why Sagas chose his name is over. Thank Goodness, even generator functions seem less scary.

Javascript (ES6) Generators — Part 1: Learn about Generators

The results of

Although everything has been rosy so far, there is somehow an unsettling feeling that fate will retaliate against you if you swim upstream. Maybe we still need a bunch of template code to enforce some standards. Perhaps we still need a framework with clear ideas. Perhaps we still need a clearly defined state tree.

What if we wanted a tool that looked like Redux but was as convenient as MobX?

If so, take a look at MobX State Tree.

With MST, we define the state tree through a dedicated API that is immutable, allowing you to roll back, serialize or recombine, and all the things you’d expect from a thoughtful state management library.

Look at the code!

import { flow } from 'mobx';
import { types } from 'mobx-state-tree';

export const TagsStoreModel = types
    .model('TagsStore', {
        results: types.array(types.string),
        isLoading: types.boolean,
        query: types.string,
    })
    .actions((self) = > ({
        loadTags: flow(function * (query: string) {
            self.query = query;
            self.isLoading = true;

            try {
                const tags = yield fetch('http://somewhere.com/api/tags');
                self.tags = tags;
            } catch (err) {
                self.err = err;
            }

            self.isLoading = false; }}));export const StoreModel = types
    .model('Store', {
        tags: TagsStoreModel,
    });

export type Store = typeof StoreModel.Type;
Copy the code

Rather than letting you do whatever you want with state management, MST defines the state tree by asking you to define it in its prescribed way. One might recall that this is MobX with chained functions instead of classes, but there’s more. This state tree cannot be modified, and a new “snapshot” is created with each change, allowing for rollback, serialization, recomposition, and all the other functionality you miss.

Looking at the remaining issues, the only low score is that this is only a partially available method for MobX, which means it does away with classes and decorators, which means TypeScript support can only do the best it can.

But even so, it’s great!

Okay, so let’s go ahead and construct the entire page.

import { Header, HeaderProps } from './header'; import { Provider, inject } from 'mobx-react'; export interface HomePageProps { headerProps? : HeaderProps; } export class HomePage extends Component<IndexPageProps> { render() { return ( <Header {... this.props.headerProps} /> <! -- the rest of the page .. -- - >); } } export const ConnectedHomePage = inject(({ tags }: Store) => ({ headerProps: { searchBoxProps: { query: tags.query, isLoading: tags.isLoading, data: tags.data, onSearch: tags.loadTags, } } })); export const tagsStore = new TagsStore(); export default () => { return ( <Provider tags={tagsStore}> <ConnectedHomePage> </Provider> ); }Copy the code

See? The components are wired the same way, so the effort involved in migrating from MobX to MST is much less than writing Redux template code.

So why didn’t we go all the way to MST?

In fact, MST is a bit overkill for our specific example. We considered it because rollback was a nice addition, but once we found Delorean we didn’t need to bother with migration. There will be situations where MobX won’t be able to handle them, but because MobX is so humble and easygoing, it won’t be too daunting to go back and use Redux again.

Anyway, MobX, we love you.

May you always be excellent.

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.