Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
MobX кроме Observer
MobX — это state management библиотека которая использует reactive programming. Observer — это декоратор для React компонентов, но MobX намного больше. Расскажу как я использую MobX инструменты и особенности.
Основные концепции MobX
MobX работает на трёх концепциях:
- Observable — состояние которое MobX отслеживает
- Computed — вычисляемые значения (автоматически кешируются)
- Reactions — побочные эффекты которые запускаются при изменении состояния
1. Observable: Создание реактивного состояния
import { observable, action, makeObservable } from 'mobx';
class UserStore {
users: User[] = [];
selectedUserId: string | null = null;
isLoading = false;
constructor() {
makeObservable(this, {
users: observable,
selectedUserId: observable,
isLoading: observable,
fetchUsers: action,
selectUser: action,
addUser: action
});
}
// Actions изменяют state
fetchUsers = async () => {
this.isLoading = true;
try {
const res = await fetch('/api/users');
this.users = await res.json();
} finally {
this.isLoading = false;
}
};
selectUser(userId: string) {
this.selectedUserId = userId;
}
addUser(user: User) {
this.users.push(user);
}
}
Мож использовать декоратор синтаксис если включить experimentalDecorators:
class UserStore {
@observable users: User[] = [];
@observable isLoading = false;
@action
fetchUsers = async () => { /* ... */ };
}
2. Computed: Вычисляемые значения
Это как useMemo но лучше — автоматически кешируется и отслеживает зависимости:
class UserStore {
@observable users: User[] = [];
@computed
get totalUsers(): number {
// Пересчитывается только если users изменился
return this.users.length;
}
@computed
get activeUsers(): User[] {
// Кешируется автоматически
return this.users.filter(u => u.isActive);
}
@computed
get usersByRole(): Map<Role, User[]> {
const map = new Map<Role, User[]>();
this.users.forEach(user => {
if (!map.has(user.role)) {
map.set(user.role, []);
}
map.get(user.role)!.push(user);
});
return map;
}
// С параметрами (создаёт мемоизированную функцию)
@computed
get usersByStatus() {
return (status: string) => this.users.filter(u => u.status === status);
}
}
Важное: computed очень эффективно если много компонентов подписаны на один store — пересчитываются только затронутые.
3. Reactions: Побочные эффекты
Это как useEffect но более мощно:
import { reaction, when, autorun } from 'mobx';
class UserStore {
@observable selectedUserId: string | null = null;
// autorun запускается при каждом изменении зависимостей
constructor() {
autorun(() => {
if (this.selectedUserId) {
console.log(`Selected user: ${this.selectedUserId}`);
}
});
}
}
// reaction: более контролируемый autorun
const disposer = reaction(
// Функция отслеживания (что смотреть)
() => userStore.selectedUserId,
// Effect (что делать когда изменится)
(userId) => {
if (userId) {
loadUserDetails(userId);
}
},
// Опции
{
delay: 300 // Debounce в 300ms
}
);
// Очищаем подписку
// disposer();
// when: выполнить когда условие станет true
when(
() => userStore.users.length > 0,
() => {
console.log('Users loaded!');
}
);
4. runInAction: Для асинхронного кода
Если не можешь использовать @action декоратор, используй runInAction:
fetch('/api/users')
.then(r => r.json())
.then(data => {
runInAction(() => {
this.users = data;
this.isLoading = false;
});
});
5. Observable Collections: Специальные типы
import { observable, ObservableMap, ObservableSet } from 'mobx';
class CartStore {
// ObservableMap для ключ-значение
items = observable.map<string, CartItem>([
['item1', { id: 'item1', qty: 2 }],
['item2', { id: 'item2', qty: 1 }]
]);
@computed
get totalPrice(): number {
let total = 0;
this.items.forEach(item => {
total += item.qty * item.price;
});
return total;
}
addItem(item: CartItem) {
this.items.set(item.id, item);
}
removeItem(itemId: string) {
this.items.delete(itemId);
}
// ObservableSet для уникальных значений
favoriteIds = observable.set<string>();
toggleFavorite(itemId: string) {
if (this.favoriteIds.has(itemId)) {
this.favoriteIds.delete(itemId);
} else {
this.favoriteIds.add(itemId);
}
}
}
6. Использование в React (не только Observer)
import { createContext, useContext } from 'react';
const StoreContext = createContext<UserStore | null>(null);
export function useUserStore() {
const store = useContext(StoreContext);
if (!store) throw new Error('Store not provided');
return store;
}
// Провайдер
export function StoreProvider({ children }) {
const store = useRef(new UserStore()).current;
return (
<StoreContext.Provider value={store}>
{children}
</StoreContext.Provider>
);
}
// Компонент с Observer
import { Observer } from 'mobx-react-lite';
function UserList() {
const store = useUserStore();
return (
<Observer>
{() => (
<div>
{store.isLoading ? (
<p>Loading...</p>
) : (
<ul>
{store.users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</div>
)}
</Observer>
);
}
7. Интеграция с API и состояние загрузки
class UserStore {
@observable users: User[] = [];
@observable error: string | null = null;
@observable state: 'idle' | 'loading' | 'success' | 'error' = 'idle';
@action
async fetchUsers(query?: string) {
this.state = 'loading';
this.error = null;
try {
const url = query ? `/api/users?q=${query}` : '/api/users';
const res = await fetch(url);
if (!res.ok) throw new Error('Failed to fetch');
runInAction(() => {
this.users = await res.json();
this.state = 'success';
});
} catch (err) {
runInAction(() => {
this.error = err.message;
this.state = 'error';
});
}
}
@computed
get isLoading(): boolean {
return this.state === 'loading';
}
@computed
get isError(): boolean {
return this.state === 'error';
}
}
8. Middleware и DevTools
MobX имеет встроенный DevTools для отладки:
import { spy, trace } from 'mobx';
// Логирование всех изменений
spy(event => {
if (event.type === 'action') {
console.log(`Action: ${event.name}`);
}
});
// Трассировка вычисленных значений
class Store {
@computed
get computed() {
trace(); // Выведет стек что вызвало пересчёт
return this.value * 2;
}
}
9. Практический пример: Полноценный Store
class TodoStore {
@observable todos: Todo[] = [];
@observable filter: 'all' | 'active' | 'completed' = 'all';
@observable isLoading = false;
constructor() {
makeObservable(this);
this.fetchTodos();
// Автосохранение при изменении
reaction(
() => this.todos.length,
() => {
this.saveTodos();
},
{ delay: 500 }
);
}
@action
async fetchTodos() {
this.isLoading = true;
try {
const res = await fetch('/api/todos');
runInAction(() => {
this.todos = await res.json();
});
} finally {
runInAction(() => {
this.isLoading = false;
});
}
}
@action
addTodo(title: string) {
this.todos.push({ id: Date.now(), title, done: false });
}
@action
toggleTodo(id: number) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.done = !todo.done;
}
@computed
get filteredTodos(): Todo[] {
switch (this.filter) {
case 'active':
return this.todos.filter(t => !t.done);
case 'completed':
return this.todos.filter(t => t.done);
default:
return this.todos;
}
}
@computed
get completedCount(): number {
return this.todos.filter(t => t.done).length;
}
@action
saveTodos() {
fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(this.todos)
});
}
}
Когда использовать MobX
- Сложное состояние с много взаимосвязей
- Много computed значений
- Нужны асинхронные reactions
- Большие приложения с много stores
- Когда Redux кажется слишком многословным
Когда NOT использовать
- Маленькие приложения (useState хватит)
- Если team не знает MobX (Redux более распространён)
- Если нужен time-travel debugging (Redux лучше)
Чем MobX лучше Redux
- Меньше boilerplate кода
- Автоматическое отслеживание зависимостей (не нужно писать dependency arrays)
- Более интуитивный API
- Мутирующий стиль вместо pure функций (иногда проще)
MobX даёт больше свободы и требует меньше кода чем Redux, но требует понимания reactive programming.