Awesome
<h1 align='center'>Mobx stores manager for React</h1> <p align="center"> <img src="./logo.png" alt="Mobx stores manager logo" width="250" height="253"> </p>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 😎
Table of contents
- Getting started
- Usage
- Support SSR
- Important Tips
- Documentation
- Example
- React Native Debug Plugin
- Bugs and feature requests
- License
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):
- 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,
},
}
}
- Define
id
for each store.
import { makeObservable } from "mobx";
class MyStore {
/**
* Define unique store id
*/
static id = 'Unique-store-id';
constructor() {
makeObservable(this, {})
}
}
- 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.
- Look at Vite demo app for a better understanding.
- Look at After.js (razzle) based project for a better understanding.
- Look at NextJS example for a better understanding (needs writing a wrapper).
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.