/react-mobx-manager

Mobx stores manager for React

Primary LanguageTypeScriptMIT LicenseMIT

Mobx stores manager for React

Mobx stores manager logo

Key features:

  • One way to escape state tree 🌲🌳🌴.
  • Ready to use with Suspense.
  • Support SSR.
  • Support render to stream.
  • Manage your Mobx stores like a boss - debug like a hacker.
  • Simple idea - simple implementation.
  • Small package size.
  • Support code splitting out of the box.
  • Access stores from other stores.
  • Can be a replacement for react context.
  • And many other nice things 😎

reliability Security Rating Maintainability Rating Vulnerabilities Bugs Lines of Code code coverage size size semantic version

Table of contents

Getting started

The React-mobx-manager package is distributed using npm, the node package manager.

npm i --save @lomray/react-mobx-manager @lomray/consistent-suspense

NOTE: this package use @lomray/consistent-suspense for generate stable id's inside Suspense.

Choose one of store id generating strategy (1 or 2 or 3):

  1. Configure your bundler to keep classnames and function names. Store id will be generated from class names (chose unique class names).
  • React: (craco or webpack config, terser options)
terserOptions.keep_classnames = true;
terserOptions.keep_fnames = true;
  • React Native: (metro bundler config: metro.config.js)
module.exports = {
  transformer: {
    minifierConfig: {
      keep_classnames: true,
      keep_fnames: true,
    },
  }
}
  1. Define id for each store.
import { makeObservable } from "mobx";

class MyStore {
  /**
   * Define unique store id
   */
  static id = 'Unique-store-id';

  constructor() {
    makeObservable(this, {})
  }
}
  1. Use Vite plugins.
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import MobxManager from '@lomray/react-mobx-manager/plugins/vite/index';

// https://vitejs.dev/config/
export default defineConfig({
  /**
   * Store id's will be generated automatically, just chill
   */
  plugins: [react(), MobxManager()]
});

/**
 * Detect mobx store:
 - by makeObservable or makeAutoObservable
 - by @mobx-store jsdoc before class
 */

Usage

Import Manager, StoreManagerProvider from @lomray/react-mobx-manager into your index file and wrap <App/> with <StoreManagerProvider/>

import React from 'react';
import ReactDOM from 'react-dom/client';
import { ConsistentSuspenseProvider } from '@lomray/consistent-suspense';
import { Manager, StoreManagerProvider, MobxLocalStorage } from '@lomray/react-mobx-manager';
import App from './app';
import MyApiClient from './services/my-api-client';
import './index.css';

const apiClient = new MyApiClient();
const storeManager = new Manager({
  storage: new MobxLocalStorage(), // optional: needs for persisting stores
  storesParams: { apiClient }, // optional: we can provide our api client for access from the each store
});

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

root.render(
  <React.StrictMode>
    <ConsistentSuspenseProvider> {/** required **/}
      <StoreManagerProvider storeManager={storeManager} shouldInit>
        <App />
      </StoreManagerProvider>
    </ConsistentSuspenseProvider>
  </React.StrictMode>,
);

Connect mobx store to the manager, and you're good to go!

import { withStores, Manager } from '@lomray/react-mobx-manager';
import { makeObservable, observable, action } from 'mobx';
import type { IConstructorParams, ClassReturnType } from '@lomray/react-mobx-manager';

/**
 * Mobx user store
 *
 * Usually store like that are related to the global store,
 * because they store information about the current user,
 * which may be needed in different places of the application.
 * 
 * You may also want to save the state of the store, for example, 
 * to local storage, so that it can be restored after page reload,
 * in this case, just export wrap export with 'persist':
 * export default Manager.persistStore(UserStore, 'user');
 */
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 behavior for global application stores
   * Default: false
   */
  static isGlobal = true;

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

  /**
   * Our API client
   */
  private apiClient: MyApiClient;

  /**
   * @constructor
   */
  constructor({ getStore, apiClient }: IConstructorParams) {
    this.apiClient = apiClient;
    // if we need, we can get a global store or store from the parent context
    // this.otherStore = getStore(SomeOtherStore);

    makeObservable(this, {
      name: observable,
      setName: action.bound,
    });
  }

  /**
   * Set user name
   */
  public setName(name: string): void {
    this.name = name;
  }

  /**
   * Example async
   * Call this func from component
   */
  public getNameFromApi = async (userId: number) => {
    const name = await this.apiClient.fetchName(userId);
    
    this.setName(name);
  }
}

/**
 * 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);

See app example for a better understanding.

Support SSR

Does this library support SSR? Short answer - yes, but we need some steps to prepare our framework.

Important Tips

  • Create global store only for e.g: application settings, logged user, theme, etc.
  • To get started, stick to the concept: Store for Component. Don't connect (through withStores) not global store to several components.

Documentation

Manager

import { Manager, MobxLocalStorage, MobxAsyncStorage } from '@lomray/react-mobx-manager';
// import AsyncStorage from '@react-native-async-storage/async-storage';

// Params
const storeManager = new Manager({
  /**
   * Optional: needs for persisting stores when you use Manager.persistStore
   * Available: MobxLocalStorage and MobxAsyncStorage
   * Default: none
   */
  storage: new MobxLocalStorage(), // React
  // storage: new MobxAsyncStorage(AsyncStorage), // React Native
  // storage: new CombinedStorage({ local: MobxAsyncStorage, cookie: CookieStorage }), // Define multiple storages
  /**
   * Optional: provide some params for access from store constructor
   * E.g. we can provide our api client for access from the store
   * Default: {}
   */
  storesParams: { apiClient },
  /**
   * Initial stores state.
   * E.g. in SSR case, restore client state from a server
   * Default: {}
   */
  initState: { storeId: { param: 'test' } },
  /**
   * Additional manager options
   */
  options: {
    /**
     * Disable persisting stores
     * E.g., it should be 'true' on a server-side (SSR)
     * Default: false
     */
    shouldDisablePersist: false,
    /**
     * Remove the initial store state after initialization
     * Default: true
     */
    shouldRemoveInitState: true,
    /**
     * Configure store destroy timers
     */
    destroyTimers: {
      init: 500,
      touched: 10000, // NOTE: set to max request timeout
      unused: 1000,
    },
  }
});

// Methods

/**
 * Optional: Call this method to load persisting data from persist storage
 * E.g., you may want manually to do this before the app render
 * Default: StoreManagerProvider does this automatically with 'shoudInit' prop
 */
await storeManager.init();

/**
 * Get all-created stores
 */
const managerStores = storeManager.getStores();

/**
 * Get specific store
 */
const store = storeManager.getStore(SomeGlobalStore);
const store2 = storeManager.getStore(SomeStore, { contextId: 'necessary-context-id' });

/**
 * Get stores context's and relations
 */
const relations = storeManager.getStoresRelations();

/**
 * Manually create stores for component
 * NOTE: 'withStores' wrapper use this method, probably you won't need it
 * WARNING: Avoid using this method directly, it may cause unexpected behavior
 */
const stores = storeManager.createStores(['someStore', MyStore], 'parent-id', 'context-id', 'suspense-id', 'HomePage', { componentProp: 'test' });

/**
 * Mount/Unmount simple stores to component
 * WARNING: Avoid using this method directly, it may cause unexpected behavior
 */
const unmount = storeManager.mountStores(stores);

/**
 * Get all-stores state
 */
const storesState = storeManager.toJSON();

/**
 * Get all persisted store's state
 */
const persistedStoresState = storeManager.toPersistedJSON();

/**
 * Get only persisted stores id's
 */
const persistedIds = Manager.getPersistedStoresIds();

/**
 * Get store observable props
 */
const observableProps = Manager.getObservableProps(store);

/**
 * Static method for access to manager from anywhere
 * NOTE: Be careful with this, especially with SSR on server-side
 */
const manager = Manager.get();

/**
 * Enable persisting state for store 
 */
const storeClass = Manager.persistStore(class MyStore {}, 'my-store');

/**
 * Choose storage and attributes
 */
const storeClass2 = Manager.persistStore(class MyStore {}, 'my-store', {
  attributes: {
    local: ['someProp'], // thees attributes will be stored in local storage
    cookie: ['specificProp'], // thees attributes will be stored in cookie storage
  }
});

withStores

import { withStores } from '@lomray/react-mobx-manager';

const stores = { 
  myStore: MyStore, 
  anotherStore: AnotherStore,
  // assign static id to future store
  demoStore: { store: SomeStore, id: 'my-id' },
  // get parent store, do this only inside children components
  parentStore: { store: SomeParentStore, isParent: true },
};

/**
 * Create and connect 'stores' to component with custom context id
 * NOTE: In most cases, you don't need to pass a third argument (contextId). 
 */
withStores(Component, stores, { customContextId: 'optional-context-id' });

StoreManagerProvider

import { StoreManagerProvider } from '@lomray/react-mobx-manager';

/**
 * Wrap your application for a pass-down store manager, context id, and init persisted state
 * 
 * shouldInit - default: false, enable initialize peristed state
 * fallback - show loader while the manager has initialized
 */
<StoreManagerProvider storeManager={storeManager} shouldInit fallback={<Loader />}>
  {/* your components */}
</StoreManagerProvider>

useStoreManager

import { useStoreManager } from '@lomray/react-mobx-manager';

const MyComponent: FC = () => {
  /**
   * Get store manager inside your function component
   */
  const storeManager = useStoreManager();
}

useStoreManagerParent

import { useStoreManagerParent } from '@lomray/react-mobx-manager';

const MyComponent: FC = () => {
  /**
   * Get parent context id
   */
  const { parentId } = useStoreManagerParent();
}

Store

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

class MyStore {
  /**
   * 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 behavior for global application stores
   * Default: false
   */
  static isGlobal = true;
    
  /**
   * Store observable state
   */
  public state = {
    name: 'Matthew',
    username: 'meow',
  }

  /**
   * @private
   */
  private readonly someParentStore: ClassReturnType<typeof SomeParentStore>;

  /**
   * @constructor
   * 
   * getStore - get parent store or global store
   * storeManager - access to store manager
   * apiClient - your custom param, see 'storesParams' in Manager
   */
  constructor({ getStore, storeManager, apiClient, componentProps }: IConstructorParams) {
    this.apiClient = apiClient;
    this.someParentStore = getStore(SomeParentStore);
    
    // In case when store is't global you can get access to component props
    console.log(componentProps);

    makeObservable(this, {
      state: observable,
    });
  }

  /**
   * Define this method if you want to do something after initialize the store
   * State restored, store ready for usage
   * Optional.
   * @private
   */
  private init(): void {
    // do something
  }

  /**
   * Define this method if you want to do something when a component with this store is unmount
   * @private
   */
  private onDestroy(): void {
    // do something
  }

  /**
   * Custom method for return store state
   * Optional.
   * Default: @see Manager.toJSON
   */
  public toJSON(): Record<string, any> {
    return { state: { username: this.state.username } };
  }
}

Lifecycles:

  • constructor
  • wakeup (restore state from persisted store)
  • init
  • onDestroy

Demo

Explore demo app to more understand.

React Native debug plugin

For debug state, you can use Reactotron debug plugin

Bugs and feature requests

Bug or a feature request, please open a new issue.

License

Made with 💚

Published under MIT License.