Skip to content

Lomray-Software/react-mobx-manager

Folders and files

NameName
Last commit message
Last commit date

Latest commit

29ed087 Â· Dec 12, 2023

History

94 Commits
Nov 27, 2023
Jul 20, 2022
Nov 27, 2023
Nov 27, 2023
Nov 27, 2023
Jul 19, 2023
Dec 12, 2023
Aug 25, 2023
Nov 27, 2023
Jul 20, 2022
Jul 20, 2022
Nov 27, 2023
Nov 27, 2023
Jul 20, 2022
Nov 27, 2023
Dec 4, 2023
May 25, 2023
Jul 20, 2022
Oct 30, 2022
Oct 30, 2022
Oct 30, 2022
Nov 27, 2023
Oct 27, 2022
Jul 20, 2022
Aug 2, 2023
Dec 6, 2023
Dec 6, 2023
Jul 20, 2022
Nov 27, 2023
Dec 6, 2023
Oct 19, 2022
Nov 27, 2023

Repository files navigation

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. 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
  /**
   * 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
 */
const stores = storeManager.createStores(['someStore', MyStore], 'parent-id', 'context-id', 'suspense-id', 'HomePage', { componentProp: 'test' });

/**
 * Mount/Unmount simple stores to component
 */
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');

withStores

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

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

const stores = { myStore: MyStore, anotherStore: AnotherStore };

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.