[go: up one dir, main page]

DEV Community

Cover image for React, TypeScript & Mobx
Nik Shevchenko
Nik Shevchenko

Posted on

React, TypeScript & Mobx

Mobx, TypeScript and React

The original post: https://shevchenkonik.com/blog/react-typescript-mobx

I want to share my R&D process of using React, TS, and Mobx. It’s about conceptual approaches to building large scalable applications. The second part of this article series will talk about building real application with 3rd services and ML preprocessing πŸ”₯

Overview

I build the web application that allows us to work with Mobx, TypeScript, React, and 3rd API Services. This article focuses on practical examples as the best way to understand the concepts of technologies and part of patterns theory.

I'll use two ways of organizing React Components for showing different ways of using stores, Class-based Components, and Functional Components with React Hooks.

Setup Application

I'll provide a short introduction to the setup process, you can skip this section if you already know it. If you need a more specific application, please use custom webpack/rollup/parcel or something else, but we'll use Create React App with TypeScript support for simple process of setup:

  • Install create-react-app with TypeScript


npx create-react-app typescript-mobx-react --template typescript


Enter fullscreen mode Exit fullscreen mode
  • Install types needed for TypeScript as development dependencies


npm i --save-dev typescript @types/node @types/react @types/react-dom @types/jest


Enter fullscreen mode Exit fullscreen mode
  • Install Mobx and its connection to React


npm i mobx-react


Enter fullscreen mode Exit fullscreen mode

App's source code is nested beneath the src directory. And structure of application will be:



β”œβ”€β”€ src
β”‚   β”œβ”€β”€ components
β”‚   β”œβ”€β”€ containers
β”‚   β”œβ”€β”€ contexts
β”‚   β”œβ”€β”€ hocs
β”‚   β”œβ”€β”€ hooks
β”‚   β”œβ”€β”€ pages
β”‚   β”œβ”€β”€ services
β”‚   β”œβ”€β”€ stores
β”‚   └── index.tsx
β”œβ”€β”€ dist
β”œβ”€β”€ node_modules
β”œβ”€β”€ README.md
β”œβ”€β”€ package.json
└── .gitignore


Enter fullscreen mode Exit fullscreen mode

Setup Services & Stores

I started developing my application by designing stores in the domain area. A few main concepts of stores composition that I need for my application:

  • Easy communication between stores.
  • Root store composition with children stores.
  • Separate communications and stores.

So I designed my application approach with MVC like Design Pattern and layered architecture as follows:

  • All backend communications (in our case we use only Spotify API as 3rd Service) are done by Service Layer.
  • The store has a state of the application so it consumes service Defining data stores. All service functions will be called in the only store, components execute Store actions when the state is needed.
  • Presentational Component can use the store directly by injecting the store or Props from Container Component can be passed in it.
  • Container or Presentational Component can invoke store actions and automatic rendering of components will be done by Mobx.

Services are a place for communication between application and Backend Services. We use this separation for a more flexible and elegant way to organize our codebase, cause if we'll use service calls inside the store then we'll find complicated stores with harder test writing process when an application will scale. Inside a store, we call the service method and update the store only inside the @action decorator of Mobx. Service methods are needed only for communication and they don’t modify Stores, we can modify observable variables only inside @action calls in Stores.

Conceptual configuration of Application

The main responsibilities of Stores:

  • Separate logic and state with components.
  • A standalone testable place that can be used in both Frontend and Backend JavaScript. And you can write really simple unit tests for your Stores & Services with any codebase size.
  • A single source of truth of Application.

An alternative, more opinionated way of organizing stores is using mobx-state-tree, which ships with cool features as structurally shared snapshots, action middlewares, JSON patch support, etc out of the box.

But Mobx-State-Tree (MST) is a like framework based on Mobx and when you start using MST you need to implement practices and API from MST. But I want to use more native way of my codebase and less overkill for my needs. If you want to see the big codebase of MST and Mobx, you can check my previous big opensource project of data labeling and annotation tools for ML on React, Mobx, and MST - Label Studio and Frontend Part of Label Studio. In MST we have many awesome things like a Tree, Snapshots, Time Travelling, etc.

Organizing Stores

The primary purpose of Mobx is to simplify the management of Stores. As application scales, the amount of state you manage will also increase. This requires some techniques to break down your application state and to divvy it up across a set of stores. Of course, putting everything in one Store is not prudent, so we apply divide-and-conquer instead.

And don't write your business logic in your components, because when you writing it, you have no way to reuse it. Better way is writing the business logic with methods in the Stores and call these methods from your containers and components.

Communication between stores

The main concept of stores communication is using Root Store as a global store where we create all different stores and pass global this inside a constructor of Root Store. Stores are the place of the truth for application.

Mobx Stores

Root Store collects all other stores in one place. If your children store needs methods or data from another store, you can pass this into a store like as User Store for easy communication between stores. The main advantages of this pattern are:

  • Simple to set up your application.
  • Supports strong typings well.
  • Makes complex unit tests easy as you just have to instantiate a root store.


/**
 * Import all your stores
 */
import { AuthStore } from './AuthStore';
import { UserStore } from './UserStore';

/**
 * Root Store Class with
 */
export class RootStore {
  authStore: AuthStore;
  userStore: UserStore;

  constructor() {
    this.authStore = new AuthStore();
    this.userStore = new UserStore(this); // Pass `this` into stores for easy communication
  }
}


Enter fullscreen mode Exit fullscreen mode

And then you can use methods from Auth Store in User Store for example:



import { observable, action } from 'mobx';
import { v4 as uuidv4 } from "uuid";
import { RootStoreModel } from './rootStore';

export interface IUserStore {
  id: string;
  name?: string;
  pic?: string;
}

export class UserStore implements IUserStore {
  private rootStore: RootStoreModel;

  @observable id = uuidv4();
  @observable name = '';
  @observable pic = '';

  constructor(rootStore?: RootStoreModel) {
    this.rootStore = rootStore;
  }

  @action getName = (name: string): void => {
    if (rootStore.authStore.id) {
      this.name = name;
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

Context Provider to pass Store

Context provides a way to pass data through the component tree without having to pass props down manually at every level. Nothing spectacular about it, better to read React Context if you are unsure though. Let's create Provider for our Application:



import React, { FC, createContext, ReactNode, ReactElement } from 'react';
import { RootStoreModel } from '../stores';

export const StoreContext = createContext<RootStoreModel>({} as RootStoreModel);

export type StoreComponent = FC<{
  store: RootStoreModel;
  children: ReactNode;
}>;

export const StoreProvider: StoreComponent = ({
  children,
  store
}): ReactElement => {
  return (
    <StoreContext.Provider value={store}>{children}</StoreContext.Provider>
  )
}


Enter fullscreen mode Exit fullscreen mode

And you can use in the entry point of Application:



import React from 'react';
import ReactDOM from 'react-dom';

import { StoreProvider } from './store/useStore';

import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <StoreProvider>
      <App />
    </StoreProvider>
  </React.StrictMode>,
  document.getElementById('root')
);


Enter fullscreen mode Exit fullscreen mode

Class and Functional Components

We can use both ways of our components β€” Class-based components and Functional components with React Hooks as a modern way to organize React Application.

If you are using use only Functional Components with React Hooks, you can use mobx-react-lite instead of mobx-react to reduce size bundle. If you are using Class-based components and Functional components, please use only mobx-react@6 which includes mobx-react-lite and uses it automatically for function components.

Custom HOC to provide Store into a Class-based Components

React Context replaces the Legacy Context which was fairly awkward to use. In simple words, React Context is used to store some data in one place and use it all over the app. Previously, Mobx had Provider/inject pattern, but currently this pattern is deprecated and we must use only one way - Context. And again, it's not mandatory to use React Context with Mobx but it's recommended now officially on the mobx-react website. You can read more info about it here - Why is Store Injecting obsolete?

And I wrote HOC (High Order Component) for support Class based Components:



import React, { ComponentType } from 'react';
/**
 * https://reactjs.org/docs/higher-order-components.html#static-methods-must-be-copied-over
*/
import hoistNonReactStatics from 'hoist-non-react-statics';

import { useStores } from '../hooks/useStores';

export type TWithStoreHOC = <P extends unknown>(
    Component: ComponentType<P>,
) => (props: P) => JSX.Element;

export const withStore: TWithStoreHOC = (WrappedComponent) => (props) => {
    const ComponentWithStore = () => {
        const store = useStores();

        return <WrappedComponent {...props} store={store} />;
    };

    ComponentWithStore.defaultProps = { ...WrappedComponent.defaultProps };
    ComponentWithStore.displayName = `WithStores(${
        WrappedComponent.name || WrappedComponent.displayName
    })`;

    hoistNonReactStatics(ComponentWithStore, WrappedComponent);

    return <ComponentWithStore />;
}


Enter fullscreen mode Exit fullscreen mode

And Class based Component will be:



import React, { Component } from 'react';
import { observer } from 'mobx-react';

import { withStore } from '../hocs';

class UserNameComponent extends Component {
    render() {
        const { store } = this.props;
        return (
            <div>{store.userStore.name}<div>
        )
    }
}

export default withStore(observer(UserNameComponent));


Enter fullscreen mode Exit fullscreen mode

This one is an elegant way to use Stores inside Components. If you want to use decorators, the code will be:



import React, { Component } from 'react';
import { observer } from 'mobx-react';

import { withStore } from '../hocs';

@withStore
@observer
class UserNameComponent extends Component {
    render() {
        const { store } = this.props;
        return (
            <div>{store.userStore.name}<div>
        )
    }
}

export default UserNameComponent;


Enter fullscreen mode Exit fullscreen mode

React Hook with Stores for Functional Components

We add a function to help us to get the stores inside the React Functional Components. Using useContext that React provides us, we pass the previously created context to it and get the value we spicified.



import { useContext } from 'react';
import { RootStore } from '../stores';
import { StoreContext } from '../contexts'

export const useStores = (): RootStore => useContext(StoreContext);


Enter fullscreen mode Exit fullscreen mode

Functional Components

If you want to use functional components, you need to use only observer function from mobx-react bindings and useStores hook of our Application:



import React from 'react';
import { observer } from 'mobx-react';

import { useStores } from '../hooks';

const FunctionalContainer: FC = observer((props) => {
  const { userStore } = useStores();

  return (
      <div>Functional Component for ${userStore.name}</div>
  )
});

export default FunctionalContainer;


Enter fullscreen mode Exit fullscreen mode

Services Layer

Services layer is the place of communications with Backend, 3rd API. Don't call your REST API Interfaces from within your stores. It really makes them hard to test your code. Instead of, please put these API Calls into extra classes (Services) and pass these instances to each store using the store's constructor. When you write tests, you can easily mock these API calls and pass your mock API Instance to each store.

For example, we need a class SpotifyService where we can use API and this class is Singleton. I use Singleton pattern because I want just a single instance available to all Stores.



import SpotifyWebApi from 'spotify-web-api-js';

export interface APISpotifyService {
    getAlbums(): Promise<void>;
}

class SpotifyService implements APISpotifyService {
    client: SpotifyWebApi.SpotifyWebApiJs;

    constructor() {
        this.client = new SpotifyWebApi();
    }

    async getAlbums(): Promise<void> {
        const albums = await this.client.getMySavedAlbums();

        return albums;
    }
}

/**
 * Export only one Instance of SpotifyService Class
*/
export const SpotifyServiceInstance = new SpotifyService();


Enter fullscreen mode Exit fullscreen mode

And you can use in your Stores in this way:



import { action } from 'mobx';
import { SpotifyServiceInstance } from '../services';

export class UserStore implements IUserStore {
@action getAlbums = (): void => {
SpotifyServiceInstance.getAlbums();
}
}

Enter fullscreen mode Exit fullscreen mode




Conclusion

To sum up, this guide shows how we can connect React with Hooks and Classes with Mobx and TypeScript. I think this combination of MVC pattern with Mobx, React and TypeScript produces highly typed, straightforward and scalable code.

The source code will be available on my github & will be under the MIT License for your using when I'll publish the second part of the article series.

I hope this walkthrough was interesting and you can could find some information that helped in your projects. If you have any feedback or something else, please write to me on twitter and we will discuss any moments.

Resources

  1. Best Practices for building large scale maintainable projects with Mobx
  2. Design Patterns – Singleton

Top comments (16)

Collapse
 
theonlybeardedbeast profile image
TheOnlyBeardedBeast

I like to use getters and setter so I can set store values directly inside the module, a setter automatically becomes an action, no need to decorate it.

  @observable protected _accessToken: Maybe<string> = null;
  @computed public get accessToken(): Maybe<string> {
    return this._accessToken;
  }
  public set accessToken(v: Maybe<string>) {
    this._accessToken = v;
  }
Collapse
 
shevchenkonik profile image
Nik Shevchenko • Edited

Thanks for your feedback!

I know this way, but I don't really understand its advantages over actions.

What is the advantage of that?

Collapse
 
theonlybeardedbeast profile image
TheOnlyBeardedBeast

Shifting action logic into your components/modules. A lot of times it can be handy, but yeah I do too prefer the way you described mobx usage, but I always crete getters and setters.

Collapse
 
gruckion profile image
Stephen Rayner

Okay finished, great work. I would love to see a follow up on unit testing. I am going over this now. I find it strange that you spoke about the importance being able to easily test your service layer but then made your service a singleton. Singletons are hard to test.

It appears you intend to only use your SpotifyService as a wrapper around the SpotifyApi and then test your service only through the store. Do you agree with this?

Also why do you need the service to be in the store? Why not just use the service directly in your component?

Thank you for taking the time to write this. It's the best article I have found so far on enterprise grade design patterns for MobX. Everyone else has half backed solutions that don't scale well.

Collapse
 
shevchenkonik profile image
Nik Shevchenko

Thanks for your feedback!

What is about SpotifyService, I want to use my wrapper around the Spotify API client in the first version of the application, you're right. If I change dependency for my solution, I need to change the only layer and don't rewrite tests.

We can use service directly in our containers (in practice, this turned out to be true) and we can separate business logic between Stores and Containers. My structure has changed after several months of work, I will update this article with the new architecture later. Shortly, new architecture includes two types of stores: Domain Stores & Local Stores. Domain Stores - stores with shared info (responses from backend, some data for many components, etc) in src/stores/, Local Stores - small stores are in the components directory for many components (containers) src/pages/status/ for example. And yes, you can use services inside components in a new architecture approach.

And I switched to Mobx 6 and some examples are outdated... I need time to rewrite the article and update code examples, architecture approaches, etc

Collapse
 
mdekaj profile image
MDEKAJ • Edited

Hi Stephen, could you tell me what RootStoreModel looks like and where it fits in? Thanks

Collapse
 
gruckion profile image
Stephen Rayner

You forgot to tell the user to install uuid @types/uuid mobx. Also you have no examples of RootStoreModel anywhere.

In rootStore.ts you are importing UserStore.ts and AuthStore.ts but then you have your UserStore.ts also importing RootStoreModel from rootStore.

/**
 * import RootStoreModel
 * Dependency cycle detected.eslintimport/no-cycle
 * Module '"./rootStore"' has no exported member 'RootStoreModel'.ts(2305)
 * Peek Problem (βŒ₯F8)
 * Quick Fix... (⌘.)
 **/
import { RootStoreModel } from "./rootStore";
Enter fullscreen mode Exit fullscreen mode
Collapse
 
staschek profile image
Staschek

This works for me:

import React, { FC, createContext, ReactNode, ReactElement } from 'react';
import { RootStore, rootStore } from '../root/root.store';

export const StoreContext = createContext<RootStore>(rootStore);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mdekaj profile image
MDEKAJ

Hi, is there any examples of RootStoreModel? I'm a bit confused where it fits in...

Collapse
 
gruckion profile image
Stephen Rayner • Edited

Wheres the src? The link is to your github not a specific project. Unsure which project the src is actually in.

github.com/search?q=user%3Ashevche...

You don't appear to have any repos that have mobx-react included within them.

Collapse
 
shevchenkonik profile image
Nik Shevchenko

I'll post the sources when I finish the application and write the second part of the article! I'm working on an application with ML enthusiasts, stay tuned!

Collapse
 
oleksandrkrupko profile image
Oleksandr Krupko

Thanks for a really good article that embraces the best practices of writing large scale applications and increases overall code quality! Keep it up, waiting for your future articles man!

Collapse
 
abhigk profile image
Abhi

I am trying to find the second part of the article. Where is the link?

Collapse
 
mrjjwright profile image
John Wright

How do you like the actual observable/computed programming model?

Collapse
 
shevchenkonik profile image
Nik Shevchenko

I use Mobx at my projects sometimes and I have never regretted choosing it. I find it convenient to develop large systems, the main point is to organize services, stores, and classes correctly. But this one is only one way to organize you know, it depends on what you compare it too ;)

Collapse
 
aralroca profile image
Aral Roca

Does anyone know of an alternative to hoist-non-react-statics to copy the react statics?

hoist-non-react-statics is heavier than my HoC lib... πŸ€ͺ