Универсальный Store в React и Mobx
MobX
это простое, опробованное в бою решение для управления состоянием вашего приложения. С помощью него можно хранить и управлять объектами приложения быстро и эффективно.
Очень часто стоит задача получить данные от сервера, сохранить их в память для отображения на клиенте. И в случае их обновления быстро и эффективно перерисовать изменения.
Самый банальный способ это использовать useState.
type TUser = { id: string; name: string } const [users, setUsers] = useState<TUser[]>([]); const loadUsers = async () => { const response = api.getUsers(); setUsers(response.data); };
Неудобство такого подхода заключается в следующем:
- Эти данные доступны только в разрезе одной страницы. Если нужно видеть эти данные на разных страницах, то придётся оборачивать в контекст.
- Чтобы обновить данные какого-то пользователя, то придётся пробегаться по массиву, искать нужный элемент и обновлять его данные. Вот так, например:
const updateUserName = (id: string, name: string) => { const user = users.find((el) => el.id === id); if (!user) return; user.name = name; setUsers(users.map((el) => el.id === id ? user : el)); };
Как видим слишком много манипуляций для обновления данных. Для удаления тоже придётся фильтровать по id
. А если при вставке новых данных надо учитывать, чтоб не получилось дубликатов, то и это дополнительная боль.
Для решения подобных задач обычно предлагается делать Хэш-таблицы, где по id сопоставляется целый объект. Например:
const users = { 'id-1': { id: 'id-1', name: 'John' }, 'id-2': { id: 'id-2', name: 'Anton' }, // ...и так далее };
Преимущество таких решений, что вы точно не создадите дубликата, если будете добавлять нового пользователя или обновить пользователя очень быстро:
// Добавляем нового пользователя users['id-3'] = { id: 'id-3', name: 'Lidia' }; // Те же данные, но не создастся новая запись users['id-3'] = { id: 'id-3', name: 'Lidia' }; // Обновляем данные пользователя const user = users['id-3']; // ... обновляем его данные users['id-3'] = user;
Думаю схема понятна. Теперь давайте приступим к MobX.
Удобным форматом реализации Хэш-таблицы может послужить Map
. Обычно у данных, которые мы храним на клиенте всегда есть поле id, по которому мы можем хранить эти данные.
Сейчас приведу сразу реализацию всего класса, а потом ниже пройдусь по всем моментам
/** * Названия моделей, которые храним в store */ export type TModelName = | 'user' | 'country' | 'region' | 'city' | 'chat' | 'chat-message' | // ...и так далее | 'user'; type TGetManyOptions<T> = { filterFn?: (model: T) => boolean; }; export class ModelStore<T extends ({ id: string } & Record<string, any>)> { public modelName: TModelName; public objects = observable(new Map<string /* model id */, T>()); constructor(modelName: TModelName) { this.modelName = modelName; makeAutoObservable(this); } public addModel = (model: T) => { this.objects.set(model.id, model); }; public removeModel = (id: string) => { this.objects.delete(id); }; public clear = () => { this.objects.clear(); }; public getOne = (id: string) => { return this.objects.get(id); }; public getMany = (options?: TGetManyOptions<T>): T[] => { let objects = Array.from(this.objects.values()); if (options?.filterFn) { objects = objects.filter(options.filterFn); } return objects; }; }
- Как видим, класс создан с Generic типами. Подразумеваем, что у моделей есть поле id.
T extends ({ id: string } & Record<string, any>
public modelName: TModelName;
— это просто название модели, которое храним. Оно необязательно для вас. Я обычно оставляю для дебага, если потребуетсяpublic objects = observable(new Map<string /* model id */, T>());
— собственно хранилище наших объектов в MobX. Map позволяет нам сделать собственно Хэш-таблицу для доступа к данным.
Все основные методы думаю не нуждаются с объяснении. Просто внимательно просмотреть и будет понятно, что они делают и для чего нужны.
Использовать данный Store дальше очень удобно.
// Файл root.store.ts type TUser = { id: string; name: string; }; const userStore = ModelStore<TUser>('user'); type TChat = { id: string; name: string; }; const chatStore = ModelStore<TChat>('chat'); // ... и так далее
// Файл users.view.tsx import React from 'react'; import { observer } from 'mobx-react-lite'; import { userStore } from './root.store'; const UsersView = observer(() => { const users = userStore.getMany(); return ( <> {users.map((el) => <div key={el.id}>{el.name}</div>)} </> ); }); export default UsersView;