Skip to content

Latest commit

 

History

History
242 lines (195 loc) · 12.9 KB

11_collections.md

File metadata and controls

242 lines (195 loc) · 12.9 KB

Колекції

Ми реалізували можливість завантажувати весь список задач. Тепер давайте реалізуємо випадок, коли у нас ще немає всіх-всіх задач, а користувач зайшов на певну групу, відповідно нам потрібно завантажити задачі конкретної групи. Але якщо ми подивимось на модель групи, то побачимо, що у нас список задач групи — це масив посилань на задачі. Тобто самі задачі мають зберігатись деінде в дереві.

const GroupModel = types.model({
  // ...решту пропсів
  
  // тут в нас масив посилань
  todos: types.array(types.reference(TodoModel)),
})
.actions((self) => ({
  // ...всі екшени
}));

Можна подумати, що масив list в TodoListModel — ідеальне місце, проте це не так. На перший погляд, ця модель призначена для того, щоб зберігати всі-всі задачі. Проте це не зовсім точно — ця модель призначена для того, щоб зберігати всі-всі задачі у певній послідовності. Саме для збереження послідовності елементів ми використовуємо масив.

Розглянемо простий приклад:

  1. Користувач запускає наш застосунок, попадає на список всіх завдань
  2. Вони завантажуються з пагінацією (так як їх може бути дуже багато) і показується тільки останніх 30, серед яких є декілька останніх створених із групи "Робота", так як сортування — по даті створення.
  3. Користувач заходить на групу "Робота"
  4. Завантажується 13 задач цієї групи
  5. Ми їх поміщаємо в масив всі завдань — в його кінець.
  6. Заходимо назад на всі завдання і бачимо дублювання елементів, тому що деякі елементи вже були в масиві, а ми їх додали наново. Крім того ми зламали пагінацію, так як тепер в нас 43 елемента в масиві, а пагінація очікує від нас offset параметр, який означає скільки елементів сервер має пропустити перш ніж віддати решту.
  7. Ми ж передаємо 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;

     // ...решту коду
  }),
}));