Ми реалізували можливість завантажувати весь список задач. Тепер давайте реалізуємо випадок, коли у нас ще немає всіх-всіх задач, а користувач зайшов на певну групу, відповідно нам потрібно завантажити задачі конкретної групи. Але якщо ми подивимось на модель групи, то побачимо, що у нас список задач групи — це масив посилань на задачі. Тобто самі задачі мають зберігатись деінде в дереві.
const GroupModel = types.model({
// ...решту пропсів
// тут в нас масив посилань
todos: types.array(types.reference(TodoModel)),
})
.actions((self) => ({
// ...всі екшени
}));
Можна подумати, що масив list
в TodoListModel — ідеальне місце, проте це не так. На перший погляд, ця модель призначена для того, щоб зберігати всі-всі задачі. Проте це не зовсім точно — ця модель призначена для того, щоб зберігати всі-всі задачі у певній послідовності.
Саме для збереження послідовності елементів ми використовуємо масив.
Розглянемо простий приклад:
- Користувач запускає наш застосунок, попадає на список всіх завдань
- Вони завантажуються з пагінацією (так як їх може бути дуже багато) і показується тільки останніх 30, серед яких є декілька останніх створених із групи "Робота", так як сортування — по даті створення.
- Користувач заходить на групу "Робота"
- Завантажується 13 задач цієї групи
- Ми їх поміщаємо в масив всі завдань — в його кінець.
- Заходимо назад на всі завдання і бачимо дублювання елементів, тому що деякі елементи вже були в масиві, а ми їх додали наново. Крім того ми зламали пагінацію, так як тепер в нас 43 елемента в масиві, а пагінація очікує від нас
offset
параметр, який означає скільки елементів сервер має пропустити перш ніж віддати решту. - Ми ж передаємо 43, що не зовсім точно, так як завантажили в цій послідовності ми тільки 30 елементів.
Для того щоб вирішити цю проблему, використовується колекція Map
(тобто фактично об'єкт типу { [key: string]: Model }
).
Mobx-state-tree надає нам можливість створити колекцію, використовуючи types.map(Model)
.
Так як у нас точно з'явиться потреба в тому, щоб створити ще якусь колекцію, прийнято групувати всі колекції в окрему модель — Entities Model.
Entities – так як кожен елемент колекції – це якась сутність (entity). Колекції сутностей прийнято називати у множині, як і ресурси в REST, тому колекцію Todo ми назвемо todos
. Ну і для зручності роботи і можливості додавати в майбутньому якісь асинхронні та синхронні екшени, ми нашу колекцію винесемо в окрему модель.
Підсумувавши вищесказане, наш код буде наступним:
const TodoCollectionModel = types.model({
// створюємо нашу колекцію
// зверніть увагу, що тут ми посилання не використовуємо
collection: types.map(TodoModel),
})
// а також декілька додаткових в'юх/екшенів, щоб зручніше було працювати
.views((store) => ({
get(id) {
// id в нас може бути number, проте get очікує string
return store.collection.get(String(id));
},
has(key) {
// id в нас може бути number, проте has очікує string
return store.collection.has(String(key));
},
find(callback) {
// чисто для того, щоб легше було знайти потрібний елемент
// коли не знаємо його id
// дозволить позбутись деякого бойлерплейту
return Array.from(store.collection.values()).find(callback);
},
}))
.actions((store) => ({
add(key, value) {
store.collection.set(String(key), value);
},
destroy(item) {
// ця утиліта з mobx-state-tree дозволяє отримати значення поля types.identifier
const id = getIdentifier(item);
store.collection.delete(String(id);
},
// цей екшен буде використано пізніше
update(key, value) {
const item = store.collection.get(String(key));
Object.assign(item, value);
},
}));
// створюємо модель, яка буде групувати всі наші колекції
const EntitiesModel = types.model({
// назва у множині
// optional використовується тому, щоб при ініціалізації Entities моделі
// нам де доводилось її ініціалізувати з `{ todos: {} }`
todos: types.optional(TodoCollectionModel, {}),
});
// і, звісно ж, додаємо її в Root
const RootModel = types.model({
todos: types.optional(TodoListModel, {}),
groups: types.optional(GroupListModel, {}),
// додаємо в кінець, так як це "стала" модель
entities: types.optional(EntitiesModel, {}),
});
Давайте також додамо код, який буде завантажувати задачі конкретної групи та зберігати їх в колекцію:
const GroupModel = types.model({
// ...решту пропсів
// тут в нас масив посилань
todos: types.array(types.reference(TodoModel)),
isLoadingTodos: false,
})
.actions((self) => ({
// ...всі екшени
fetchTodos: flow(function* fetchTodosFlow() {
try {
self.isLoadingTodos = true;
// завантажуємо масив задач
const todos = yield Api.Group.fetchTodos(self.id);
// отримуємо посилання на колекцію
const collection = getRoot(self).entities.todos;
const ids = todos.map((todo) => {
// зберігаємо в колекцію
collection.add(todo.id, todo);
// повертаємо ідентифікатор, щоб використати як посилання
return todo.id;
});
// записуємо масив посилань на задачі в нашу модель
self.todos = ids;
self.isLoadingTodos = false;
} catch (err) {
self.isLoadingTodos = false;
console.log(err);
}
}),
}));
Вирішуючи проблему зберігання задач, які ми завантажуємо для кожної групи, особливо уважні могли зрозуміти, що ми створили собі нову проблему. Яку? Давайте розберемось.
Задачі, завантажені для групи, ми зберігаємо в TodoCollectionModel
. Проте наш list
в TodoListModel
містить також задачі. І після того як ми завантажили список задач в TodoListModel
, а тоді спробували завантажити список задач конкретної групи, є велика ймовірність, що ми будемо зберігати одну й ту ж сутність Todo в двох місцях у дереві — в колекції TodoCollectionModel
та масиві list
моделі TodoListModel
. Тому якщо ми використовуємо для сутності колекцію — потрібно її використовувати всюди. Тобто тепер у всіх місцях, де ми використовували TodoModel
напряму нам потрібно використовувати посилання на неї, а саму модель зберігати в колекції.
// оновимо реалізацію моделі
const TodoListModel = types.model({
// тепер використовуємо посилання
list: types.array(types.reference(TodoModel)),
})
.views((self) => ({ /* ...тут наша в'юха get favorites */ }))
.actions((self) => ({
/* ...тут екшн додавання задачі */
// зверніть увагу що це генератор — "function*"
fetchTodos: flow(function* fetchTodosFlow() {
try {
self.isLoading = true;
// замість `await` використовуємо `yield`
const todos = yield Api.Todos.fetchList();
// отримуємо посилання на колекцію
const collection = getRoot(self).entities.todos;
const ids = todos.map((todo) => {
// зберігаємо в колекцію
collection.add(todo.id, todo);
// повертаємо ідентифікатор, щоб використати як посилання
return todo.id;
});
// записуємо масив посилань на задачі в нашу модель
self.list = ids;
self.isLoading = false;
} catch (err) {
self.isLoading = false;
console.log(err);
}
}),
}));
Проте тепер у нас появляється дублювання коду. Додавання в колекцію та формування масиву посилань ми фактично "скопіпастили", що не є хорошою практикою. Так як це код, який ми точно ще не раз напишемо, можна його винести в окрему функцію, а ще краще — екшен в EntitiesModel
.
const EntitiesModel = types.model({
todos: types.optional(TodoCollectionModel, {}),
})
.actions((self) => ({
// так як наш екшен буде об'єднувати дані, які передаються, в колекція
// ми його назвемо merge
// приймати він буде назву колекції та сутність чи їх масив
merge(collectionName, items) {
// отримуємо колекцію
const collection = self[collectionName];
// якщо ми передали масив елементів, то записуємо масив
if (Array.isArray(items)) {
const ids = items.map((item) => {
// зберігаємо в колекцію
collection.add(item.id, item);
// повертаємо ідентифікатор, щоб використати як посилання
return item.id;
});
// повертаємо масив ідентифікаторів
return ids;
}
// в іншому випадку записуємо лиш один елемент
collection.add(item.id, item);
// і повертаємо його ідентифікатор
return item.id;
},
}));
// оновимо реалізацію моделі
const TodoListModel = types.model({
list: types.array(types.reference(TodoModel)),
})
.actions((self) => ({
/* ...тут екшн додавання задачі */
// зверніть увагу що це генератор — "function*"
fetchTodos: flow(function* fetchTodosFlow() {
// ...решту коду
const todos = yield Api.Todos.fetchList();
// записуємо сутності в колекцію та отримуємо їх посилання
const ids = getRoot(self).entities.merge('todos', todos);
// записуємо масив посилань на задачі в нашу модель
self.list = ids;
// ...решту коду
}),
}));