MobX Stores
Class-based Store
Заголовок раздела «Class-based Store»Базовый паттерн — класс с состоянием, computed и actions:
import { makeAutoObservable, runInAction } from 'mobx';
interface User { id: number; name: string; email: string; role: 'admin' | 'user';}
class UserStore { users: User[] = []; selectedId: number | null = null; isLoading = false; error: string | null = null;
constructor() { makeAutoObservable(this); }
// computed get selectedUser(): User | null { if (this.selectedId === null) return null; return this.users.find(u => u.id === this.selectedId) ?? null; }
get admins(): User[] { return this.users.filter(u => u.role === 'admin'); }
// actions select(id: number) { this.selectedId = id; }
deselect() { this.selectedId = null; }
// async action async loadUsers() { this.isLoading = true; this.error = null;
try { const users = await userApi.getAll(); runInAction(() => { this.users = users; this.isLoading = false; }); } catch (err) { runInAction(() => { this.error = err instanceof Error ? err.message : 'Неизвестная ошибка'; this.isLoading = false; }); } }
async updateUser(id: number, data: Partial<User>) { const updated = await userApi.update(id, data); runInAction(() => { const idx = this.users.findIndex(u => u.id === id); if (idx !== -1) this.users[idx] = updated; }); }
async deleteUser(id: number) { await userApi.delete(id); runInAction(() => { this.users = this.users.filter(u => u.id !== id); if (this.selectedId === id) this.selectedId = null; }); }}RootStore паттерн
Заголовок раздела «RootStore паттерн»Центральный объект, который содержит все сторы и обеспечивает доступ друг к другу:
class AuthStore { token: string | null = null; currentUser: User | null = null;
constructor(private root: RootStore) { makeAutoObservable(this); }
get isAuthenticated() { return this.token !== null; }
async login(email: string, password: string) { const { token, user } = await authApi.login(email, password); runInAction(() => { this.token = token; this.currentUser = user; }); // Доступ к другим сторам через root await this.root.userStore.loadUsers(); }
logout() { this.token = null; this.currentUser = null; this.root.userStore.users = []; // очищаем данные при выходе }}
class UserStore { users: User[] = [];
constructor(private root: RootStore) { makeAutoObservable(this); }
// Использование данных из другого стора get currentUserProfile() { const currentId = this.root.authStore.currentUser?.id; if (!currentId) return null; return this.users.find(u => u.id === currentId) ?? null; }
async loadUsers() { const token = this.root.authStore.token; if (!token) throw new Error('Not authenticated'); const users = await userApi.getAll(token); runInAction(() => { this.users = users; }); }}
class RootStore { authStore: AuthStore; userStore: UserStore;
constructor() { this.authStore = new AuthStore(this); this.userStore = new UserStore(this); }}
// Создаём один экземплярexport const rootStore = new RootStore();Dependency Injection через Context
Заголовок раздела «Dependency Injection через Context»Передача стора через React Context — лучший способ:
import React, { createContext, useContext } from 'react';
// Создание и передача через контекстconst RootStoreContext = createContext<RootStore | null>(null);
export function RootStoreProvider({ children }: React.PropsWithChildren) { // Создаём один раз const [store] = useState(() => new RootStore());
return ( <RootStoreContext.Provider value={store}> {children} </RootStoreContext.Provider> );}
// Типобезопасный хукexport function useRootStore(): RootStore { const store = useContext(RootStoreContext); if (!store) throw new Error('useRootStore must be used within RootStoreProvider'); return store;}
// Хуки для отдельных сторовexport function useAuthStore() { return useRootStore().authStore; }export function useUserStore() { return useRootStore().userStore; }
// Использование в компонентеfunction UserList() { const userStore = useUserStore();
useEffect(() => { userStore.loadUsers(); }, [userStore]);
return ( <observer(function UserListInner() { if (userStore.isLoading) return <Spinner />; return <ul>{userStore.users.map(u => <li key={u.id}>{u.name}</li>)}</ul>; }) );}Персистентность Store
Заголовок раздела «Персистентность Store»import { autorun, toJS } from 'mobx';
class SettingsStore { theme: 'light' | 'dark' = 'light'; language = 'ru'; notifications = true;
constructor() { makeAutoObservable(this); this.load(); }
private load() { try { const saved = localStorage.getItem('settings'); if (saved) { const data = JSON.parse(saved); Object.assign(this, data); } } catch { /* ignore */ } }
// Автосохранение при каждом изменении private setupPersistence() { autorun(() => { localStorage.setItem('settings', JSON.stringify(toJS(this))); }); }
setTheme(theme: 'light' | 'dark') { this.theme = theme; } setLanguage(language: string) { this.language = language; }}Тестирование Store
Заголовок раздела «Тестирование Store»import { UserStore, RootStore } from '../user-store';
describe('UserStore', () => { let store: UserStore; let root: RootStore;
beforeEach(() => { root = new RootStore(); store = root.userStore; });
it('should load users', async () => { // Mock API vi.spyOn(userApi, 'getAll').mockResolvedValue([ { id: 1, name: 'Иван', email: 'ivan@test.ru', role: 'user' } ]);
await store.loadUsers();
expect(store.users).toHaveLength(1); expect(store.isLoading).toBe(false); expect(store.error).toBeNull(); });
it('computed admins filters correctly', () => { store.users = [ { id: 1, name: 'Иван', email: '', role: 'user' }, { id: 2, name: 'Пётр', email: '', role: 'admin' }, ]; expect(store.admins).toHaveLength(1); expect(store.admins[0].name).toBe('Пётр'); });});