5月31日 15:55
How to use MobX for state management in large applications?
In large applications, using MobX for state management requires consideration of architecture design, modularity, and maintainability. Here are best practices for building large-scale MobX applications:
1. Store Architecture Design
Single Store vs Multiple Stores
Single Store
javascriptclass RootStore { @observable user = null; @observable products = []; @observable cart = []; @observable orders = []; constructor() { makeAutoObservable(this); } }
Pros:
- Simple and direct
- Easy to debug
- Centralized state management
Cons:
- File may become too large
- Difficult to modularize
- Hard for team collaboration
Multiple Stores
javascriptclass UserStore { @observable user = null; @observable isAuthenticated = false; constructor(rootStore) { this.rootStore = rootStore; makeAutoObservable(this); } @action login = async (credentials) => { const user = await api.login(credentials); this.user = user; this.isAuthenticated = true; }; } class ProductStore { @observable products = []; @observable loading = false; constructor(rootStore) { this.rootStore = rootStore; makeAutoObservable(this); } @computed get featuredProducts() { return this.products.filter(p => p.featured); } } class CartStore { @observable items = []; constructor(rootStore) { this.rootStore = rootStore; makeAutoObservable(this); } @computed get total() { return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0); } @action addItem = (product) => { this.items.push({ ...product, quantity: 1 }); }; }
Pros:
- Clear modularity
- Easy for team collaboration
- Better code organization
Cons:
- Need to handle dependencies between stores
- Debugging may be more complex
2. Store Communication
Sharing References Through RootStore
javascriptclass RootStore { constructor() { this.userStore = new UserStore(this); this.productStore = new ProductStore(this); this.cartStore = new CartStore(this); makeAutoObservable(this); } } // Access UserStore in CartStore class CartStore { @action checkout = async () => { const user = this.rootStore.userStore.user; if (!user) { throw new Error('User not logged in'); } await api.createOrder(this.items, user.id); }; }
Using Dependency Injection
javascriptclass CartStore { constructor(userStore) { this.userStore = userStore; makeAutoObservable(this); } @action checkout = async () => { const user = this.userStore.user; // ... }; } // Create store const userStore = new UserStore(); const cartStore = new CartStore(userStore);
Using Event Bus
javascriptclass EventBus { @observable events = []; emit(event, data) { this.events.push({ event, data, timestamp: Date.now() }); } on(event, callback) { return reaction( () => this.events.filter(e => e.event === event), (events) => { if (events.length > 0) { callback(events[events.length - 1].data); } } ); } }
3. State Persistence
Using localStorage
javascriptclass StorageStore { @observable state = {}; constructor(key, initialState = {}) { this.key = key; this.state = this.loadState() || initialState; makeAutoObservable(this); // Auto save autorun(() => { this.saveState(toJS(this.state)); }); } loadState() { try { const saved = localStorage.getItem(this.key); return saved ? JSON.parse(saved) : null; } catch (error) { console.error('Failed to load state:', error); return null; } } saveState(state) { try { localStorage.setItem(this.key, JSON.stringify(state)); } catch (error) { console.error('Failed to save state:', error); } } } // Usage const appStore = new StorageStore('appState', { user: null, theme: 'light', language: 'en' });
Using sessionStorage
javascriptclass SessionStorageStore extends StorageStore { loadState() { try { const saved = sessionStorage.getItem(this.key); return saved ? JSON.parse(saved) : null; } catch (error) { return null; } } saveState(state) { try { sessionStorage.setItem(this.key, JSON.stringify(state)); } catch (error) { console.error('Failed to save state:', error); } } }
Using IndexedDB
javascriptclass IndexedDBStore { @observable data = null; constructor(dbName, storeName) { this.dbName = dbName; this.storeName = storeName; makeAutoObservable(this); this.initDB(); } async initDB() { return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, 1); request.onerror = () => reject(request.error); request.onsuccess = () => { this.db = request.result; this.loadData(); resolve(); }; request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains(this.storeName)) { db.createObjectStore(this.storeName); } }; }); } async loadData() { const transaction = this.db.transaction([this.storeName], 'readonly'); const store = transaction.objectStore(this.storeName); const request = store.get('data'); request.onsuccess = () => { runInAction(() => { this.data = request.result; }); }; } async saveData(data) { const transaction = this.db.transaction([this.storeName], 'readwrite'); const store = transaction.objectStore(this.storeName); store.put(data, 'data'); } }
4. Code Organization
Organize by Feature Module
shellsrc/ stores/ user/ index.js actions.js getters.js product/ index.js actions.js getters.js cart/ index.js actions.js getters.js index.js
Organize by Type
shellsrc/ stores/ observables/ user.js products.js cart.js actions/ userActions.js productActions.js cartActions.js computed/ userComputed.js productComputed.js cartComputed.js index.js
5. Type Safety (TypeScript)
Define Store Interface
typescriptinterface IUserStore { user: User | null; isAuthenticated: boolean; login: (credentials: Credentials) => Promise<void>; logout: () => void; } class UserStore implements IUserStore { @observable user: User | null = null; @observable isAuthenticated: boolean = false; constructor(private rootStore: RootStore) { makeAutoObservable(this); } @action login = async (credentials: Credentials): Promise<void> => { const user = await api.login(credentials); this.user = user; this.isAuthenticated = true; }; @action logout = (): void => { this.user = null; this.isAuthenticated = false; }; }
Define RootStore
typescriptinterface IRootStore { userStore: IUserStore; productStore: IProductStore; cartStore: ICartStore; } class RootStore implements IRootStore { userStore: IUserStore; productStore: IProductStore; cartStore: ICartStore; constructor() { this.userStore = new UserStore(this); this.productStore = new ProductStore(this); this.cartStore = new CartStore(this); makeAutoObservable(this); } }
6. Testing
Test Store
javascriptimport { UserStore } from './UserStore'; describe('UserStore', () => { let store; beforeEach(() => { store = new UserStore(); }); it('should initialize with default values', () => { expect(store.user).toBeNull(); expect(store.isAuthenticated).toBe(false); }); it('should login user', async () => { await store.login({ username: 'test', password: 'test' }); expect(store.user).not.toBeNull(); expect(store.isAuthenticated).toBe(true); }); it('should logout user', () => { store.user = { id: 1, name: 'Test' }; store.isAuthenticated = true; store.logout(); expect(store.user).toBeNull(); expect(store.isAuthenticated).toBe(false); }); });
Test Component
javascriptimport { render, screen } from '@testing-library/react'; import { observer } from 'mobx-react-lite'; import { UserStore } from './UserStore'; const TestComponent = observer(({ store }) => ( <div> {store.isAuthenticated ? ( <div>Welcome, {store.user?.name}</div> ) : ( <div>Please login</div> )} </div> )); describe('TestComponent', () => { it('should show login message when not authenticated', () => { const store = new UserStore(); render(<TestComponent store={store} />); expect(screen.getByText('Please login')).toBeInTheDocument(); }); it('should show welcome message when authenticated', () => { const store = new UserStore(); store.user = { id: 1, name: 'Test' }; store.isAuthenticated = true; render(<TestComponent store={store} />); expect(screen.getByText('Welcome, Test')).toBeInTheDocument(); }); });
7. Performance Optimization
Use computed Caching
javascriptclass ProductStore { @observable products = []; @observable filters = { category: null, priceRange: null, search: '' }; @computed get filteredProducts() { let result = this.products; if (this.filters.category) { result = result.filter(p => p.category === this.filters.category); } if (this.filters.priceRange) { result = result.filter(p => p.price >= this.filters.priceRange.min && p.price <= this.filters.priceRange.max ); } if (this.filters.search) { const search = this.filters.search.toLowerCase(); result = result.filter(p => p.name.toLowerCase().includes(search) ); } return result; } }
Use reaction for Delayed Execution
javascriptclass SearchStore { @observable query = ''; @observable results = []; @observable loading = false; constructor() { makeAutoObservable(this); reaction( () => this.query, (query) => { this.performSearch(query); }, { delay: 300 } ); } @action performSearch = async (query) => { if (!query) { this.results = []; return; } this.loading = true; try { this.results = await api.search(query); } finally { this.loading = false; } }; }
8. Error Handling
Global Error Handling
javascriptclass ErrorStore { @observable errors = []; @action addError = (error) => { this.errors.push({ message: error.message, stack: error.stack, timestamp: Date.now() }); }; @action clearErrors = () => { this.errors = []; }; } // Integrate in RootStore class RootStore { constructor() { this.errorStore = new ErrorStore(); this.userStore = new UserStore(this); this.productStore = new ProductStore(this); // Global error capture window.addEventListener('error', (event) => { this.errorStore.addError(event.error); }); window.addEventListener('unhandledrejection', (event) => { this.errorStore.addError(event.reason); }); } }
Summary
Key points for building large-scale MobX applications:
- Reasonable Store architecture design (single vs multiple)
- Handle store communication
- Implement state persistence
- Good code organization
- Use TypeScript for type safety
- Write comprehensive tests
- Optimize performance
- Complete error handling
Following these best practices can build maintainable, scalable large-scale MobX applications.