DEV Community

Mikhail Yarmaliuk for Lomray Software LLC

Posted on

Creating Mobx Stores Manager: A Fresh Perspective

Mobx store manager logo


Manage your Mobx stores like a boss — debug like a hacker.


Hello there!👋 I'm Mikhail, the Chief Technology Officer👮‍♀️ at Lomray Software. Today, I'm excited to share our decision in mobx for managing stores.

In a sea🌊 of existing solutions, you might be wondering if there's room for one more. Allow me to assure you — this isn't just another rehash of the same old state tree concept. While established approaches certainly have their merits, I propose an alternative. As the saying goes, "two heads are better than one" and in that spirit, I present to you an innovative perspective that I believe everyone should consider.

A New Take on State Management:
If you're anything like me, you've grown somewhat weary of the conventional state tree🌲 models. Whether it's the state tree of Redux or the Mobx approach, it's hard to escape their omnipresence, even in basic Google searches. Seeking something both novel and familiar, I embarked on a journey🔍 to reimagine the paradigm, particularly after transitioning from Redux to Mobx not long ago.

import { combineReducers } from 'redux';
import AppInfoReducer from '@store/modules/app-info/reducer';
import FormApplicationReducer from '@store/modules/form-application/reducer';
import FormPopupReducer from '@store/modules/form-popup/reducer';
import FormQuizReducer from '@store/modules/form-quiz/reducer';
import FormSubscribeReducer from '@store/modules/form-subscribe/reducer';
import PageMetaReducer from '@store/modules/page-meta/reducer';
import BlogDetailsReducer from '@store/modules/pages/blog-details/reducer';
import BlogInfoReducer from '@store/modules/pages/blog/info/reducer';
import BlogListReducer from '@store/modules/pages/blog/list/reducer';
import BlogTagsReducer from '@store/modules/pages/blog/tags/reducer';
import Think3DetailsReducer from '@store/modules/pages/think3-details/reducer';
import Think3CitiesReducer from '@store/modules/pages/think3/cities/reducer';
import Think3InfoReducer from '@store/modules/pages/think3/info/reducer';
import Think3ListReducer from '@store/modules/pages/think3/list/reducer';
import Think3TagsReducer from '@store/modules/pages/think3/tags/reducer';
import Think1DetailsReducer from '@store/modules/pages/think1-details/reducer';
import Think1InfoReducer from '@store/modules/pages/think1/info/reducer';
import Think1ListReducer from '@store/modules/pages/think1/list/reducer';
import Think1TagsReducer from '@store/modules/pages/think1/tags/reducer';
import ContactPageReducer from '@store/modules/pages/contact/reducer';
import HomeGraphReducer from '@store/modules/pages/home/graph/reducer';
import HomePageReducer from '@store/modules/pages/home/info/reducer';
import Think2DetailsReducer from '@store/modules/pages/think2-details/reducer';
import Think2InfoReducer from '@store/modules/pages/think2/info/reducer';
import Think2ListReducer from '@store/modules/pages/think2/list/reducer';
import Think2TagsReducer from '@store/modules/pages/think2/tags/reducer';
import OurTeamPageReducer from '@store/modules/pages/our-team/reducer';
import PrivacyPolicyReducer from '@store/modules/pages/privacy-policy/reducer';
import SearchReducer from '@store/modules/pages/search/reducer';
import FormCvReducer from '@store/modules/post-cv/reducer';

const reducers = {
  pages: combineReducers({
    home: combineReducers({
      info: HomePageReducer,
      graph: HomeGraphReducer,
    }),
    think1: combineReducers({
      info: Think1InfoReducer,
      list: Think1ListReducer,
      tags: Think1TagsReducer,
    }),
    think1Details: Think1DetailsReducer,
    think2: combineReducers({
      info: Think2InfoReducer,
      list: Think2ListReducer,
      tags: Think2TagsReducer,
    }),
    think2Details: Think2DetailsReducer,
    ourTeam: OurTeamPageReducer,
    think3: combineReducers({
      info: Think3InfoReducer,
      tags: Think3TagsReducer,
      list: Think3ListReducer,
      cities: Think3CitiesReducer,
    }),
    think3Details: Think3DetailsReducer,
    blog: combineReducers({
      info: BlogInfoReducer,
      list: BlogListReducer,
      tags: BlogTagsReducer,
    }),
    blogDetails: BlogDetailsReducer,
    contact: ContactPageReducer,
    search: SearchReducer,
    privacyPolicy: PrivacyPolicyReducer,
  }),
  appInfo: AppInfoReducer,
  formSubscribe: FormSubscribeReducer,
  formCv: FormCvReducer,
  pageMeta: PageMetaReducer,
  formPopup: FormPopupReducer,
  formApplication: FormApplicationReducer,
  formQuiz: FormQuizReducer,
};

export default reducers;
Enter fullscreen mode Exit fullscreen mode
It’s small application😰 (some names replaced)

A Challenge of Duplication:
Let's be candid—have you ever sought a solution that facilitates the concurrent use of components connected to a store? Have you faced the task of running a connected component multiple times across various pages📑 in your React app or screens in React Native? As you survey your app, you might find the state tree🌴🌴🌴 growing unwieldy, leading to complex code (as demonstrated above). While alternate libraries could potentially address this, I wanted a different solution—one that aligned with the class-based approach that Mobx employs.

State tree problem image

Try to run same page twice, which connected to one state tree node

Embracing Mobx's Strengths:
I've always been drawn✍️ to Mobx's class-based methodology. The advantages offered by classes are difficult to ignore, especially when it comes to business logic and state management. However, a curious cycle emerges—while attempting to streamline one library's functionality, we often find ourselves introducing another, and then another. This chain of dependencies complicates development, requiring mastery of multiple tools and their nuances. Furthermore, onboarding new developers to this "monster"🐙 can be a daunting task.

Proble with documentation image

Try to write app with new state management library

The Quest for Centralization:
Although Mobx's documentation showcases rudimentary store usage examples, it becomes apparent that a centralized approach is missing. Interactions between stores and certain debugging tools may feel reminiscent of Redux—an acquaintance many of us share. This lack of seamless integration and default code splitting options is evident upon deeper🪔 exploration.

/**
 * Example redux
 */

/**
 * Action types
 */
enum EXAMPLE_ACTION_TYPE {
  EXAMPLE_USER_GET = 'EXAMPLE_USER_GET',
  EXAMPLE_USER_GET_SUCCESS = 'EXAMPLE_USER_GET_SUCCESS',
  EXAMPLE_USER_GET_ERROR = 'EXAMPLE_USER_GET_ERROR',
}

/**
 * Action creators
 */

const getUser = (): IAction<EXAMPLE_ACTION_TYPE> => ({
  type: EXAMPLE_ACTION_TYPE.EXAMPLE_USER_GET,
  payload: {},
});

const getUserSuccess = (user: Record<string, any>): IAction<EXAMPLE_ACTION_TYPE> => ({
  type: EXAMPLE_ACTION_TYPE.EXAMPLE_USER_GET_SUCCESS,
  payload: { user },
});

const getUserError = (message: string): IAction<EXAMPLE_ACTION_TYPE> => ({
  type: EXAMPLE_ACTION_TYPE.EXAMPLE_USER_GET_ERROR,
  payload: { message },
});

/**
 * Reducer
 */

const initState = {
  fetching: false,
  error: null,
  result: null,
};

const reducer = (
  state = initState,
  { type, payload }: IAction<EXAMPLE_ACTION_TYPE> = {},
): IAppInfoState => {
  switch (type) {
    case EXAMPLE_ACTION_TYPE.EXAMPLE_USER_GET:
      return { ...initState, fetching: true };

    case EXAMPLE_ACTION_TYPE.EXAMPLE_USER_GET_ERROR:
      return { ...initState, error: payload.message };

    case EXAMPLE_ACTION_TYPE.EXAMPLE_USER_GET_SUCCESS:
      return { ...initState, result: payload.user };

    default:
      return { ...state };
  }
};

/**
 * Saga
 */

function* getUserSaga(): SagaIterator {
  try {
    // axios request
    const { data } = yield call(() => axios.get('/users/123'));

    yield put(getUserSuccess(data));
  } catch (e) {
    yield put(getUserError(e.message));
  }
}

export default [
  takeLatest(EXAMPLE_ACTION_TYPE.EXAMPLE_USER_GET, getUserSaga),
];
Enter fullscreen mode Exit fullscreen mode
/**
 * Example Mobx state tree
 */

import { values } from "mobx"
import { types, getParent, flow } from "mobx-state-tree"

export const User = types.model("User", {
  id: types.identifier,
  name: types.string,
})

export const UserStore = types
  .model("UserStore", {
    fetching: false,
    error: null,
    user: types.reference(User),
  })
  .actions((self) => {
    function setIsFetching(fetching) {
      self.fetching = fetching
    }

    function setUser(user) {
      self.user = user;
    }

    function setError(error) {
      self.error = error;
    }

    function getUser() {
      try {
        setIsFetching(true);
        const { data } = await axios.get("/users/123")
        setUser(data);
        setIsFetching(false)
      } catch (err) {
        setError("Failed to load user")
      }
    })

    return {
      getUser,
      setUser
    }
  })
Enter fullscreen mode Exit fullscreen mode
import { makeObservable, observable, action } from "mobx"

interface IUser {
  id: number;
  name: string;
}

class UserStore {
  user: IUser | null = null;
  fetching = false;
  error: string | null = null;

  constructor() {
    makeObservable(this, {
      user: observable,
      fetching: observable,
      error: observable,
      setIsFetching: action.bound,
      setError: action.bound,
      setUser: action.bound,
    })
  }

  setIsFetching(fetching: boolean) {
    this.fetching = fetching;
  }

  setError(error: string | null) {
    this.error = error;
  }

  setUser(user) {
    this.user = user;
  }

  getUser = async () => {
    this.setIsFetching(true);

    try {
      const {data} = await axios.get("/users/123")
      this.setUser(data);
    } catch (e) {
      this.setError("Failed to load user")
    } finally {
      this.setIsFetching(false);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Introducing React Mobx Manager:
Enter the solution: React Mobx Manager. Picture this as a singleton—an information repository housing details about all instantiated stores, their contexts, and the linked component names. With this approach, you sidestep the need to build and configure an entire state tree from scratch. Instead, you create dedicated stores for each component, only as necessary. Furthermore, accessing a pre-existing store from another store becomes a breeze🌬💧. The need for complex code splitting is diminished through clever store techniques that mimic React context functionality. You can even concurrently mount identical pages or screens while retaining state integrity. For those interested, a Reactotron plugin is available, and work on browser🌐 extensions is underway.

Visualizing the Concept:
To illustrate this concept visually, consider the following diagram:

How Mobx Manager works image

Mobx Store Manager idea

First we need to create a manager and wrap our application in a context (entrypoint src/index.tsx)

import React from 'react';
import ReactDOM from 'react-dom/client';
import { Manager, StoreManagerProvider } from '@lomray/react-mobx-manager';
import User from './user';

const storeManager = new Manager();

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);

root.render(
  <React.StrictMode>
    <StoreManagerProvider storeManager={storeManager} shouldInit fallback={<div>Loading…</div>}>
      <User />
    </StoreManagerProvider>
  </React.StrictMode>,
);
Enter fullscreen mode Exit fullscreen mode

Then we create a store and connect it to the component:

import type { FC } from 'react';
import { makeObservable, observable } from 'mobx';
import type { IConstructorParams, StoresType } from '@lomray/react-mobx-manager';
import { withStores } from '@lomray/react-mobx-manager';

class UserStore {
  /**
   * Required only if we don't configure our bundler to keep classnames and function names
   * Default: current class name
   */
  static id = 'user';

  /**
   * You can also enable 'singleton' behavior for global application stores
   * Default: false
   */
  static isSingleton = true;

  /**
   * Our state
   */
  public name = 'Matthew'

  /**
   * @constructor
   */
  constructor(params: IConstructorParams) {
    makeObservable(this, {
     name: observable,
   });
  }
}

/**
 * Define stores for component
 */
const stores = {
 userStore: UserStore
};

/**
 * Support typescript
 */
type TProps = StoresType <typeof stores>;

/**
 * User component
 */
const User: FC<TProps> = ({ userStore: { name } }) => {
 return (
   <div>{name}</div>
 )
}

/**
 * Connect stores to component
 */
export default withStores(User, stores);
Enter fullscreen mode Exit fullscreen mode

Done!✅

I’ll just show how it all looks in the reactotron debugger:

Reactotron mobx manager demo image

If this has piqued your interest, waste no time in diving🤿 into the mechanics. Allow me to provide you with a practical example.

Conclusion:
React Mobx Manager isn't gunning for any awards. Rather, it's an alternative perspective and a personal choice. Whether you embrace it or not, the decision ultimately rests with you. I genuinely appreciate your time spent on this article and am eagerly open to any ideas or suggestions you may have on this matter.

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.