Универсальный 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;