Ещё один способ отслеживания вложенных структур данных

@tracked состояние массива или объекта внутри компонента Glimmer не вызывает обновлений при изменении элемента массива/объекта.

В этой теме обсуждаются некоторые варианты для корректного обновления представления. Я хотел добавить ещё один способ, так как описанные там варианты мне не подошли.

Вот как я решил эту проблему: вместо присваивания одного элемента за раз, присваивайте весь массив целиком. Этот же подход работает с объектами и любой другой структурой данных.

В компоненте Glimmer:

@tracked categories = [];

constructor() {
  super(...arguments);
  this.loadAllData();
}

async loadAllData() {
  const categories = []
  const waiting = this.siteSettings.county_fence_print_categories.split('|').map(async (cid, index) => {
    const data = await this.loadData(`/c/${cid}/l/latest.json?period=monthly`)
    return data.topic_list.topics
  })

  // ОБРАТИТЕ ВНИМАНИЕ НА ЭТУ СТРОКУ
  // Поскольку мы используем присваивание (=) напрямую к массиву, это вызовет обновление.
  // В то время как добавление элементов через push или установка по индексу не вызовут обновления.
  this.categories = await Promise.all(waiting)
}

async loadData(url) {
  try {
    const data = await ajax(url);
    //console.log(`Получены данные для ${url}:`, data)
    return data
  } catch (error) {
    console.error(`Не удалось получить данные для ${url}:`, error);
  }
}

Вы также можете сделать это с частичным обновлением. Главное, чтобы JavaScript обнаружил, что присваивается новое значение. Таким образом, .concat сработает, так как он создаёт новый массив, отличный от старого:

this.categories = this.categories.concat(newValue)

Использование .push, скорее всего, не сработает, так как результирующий массив будет иметь тот же идентификатор объекта, что и исходный, поэтому JavaScript подумает, что ничего не изменилось.

this.categories.push(newValue)
this.categories = this.categories

Если вы работаете с объектом, простой способ создать новый — использовать Object.assign(), который создаёт новую, расширенную структуру данных, аналогично .concat выше.

Это уже давно известно для других фреймворков, таких как Vue.js — можно найти темы об этом, уходящие на 10 лет назад. Это связано с тем, как это реализовано в JavaScript, вероятно, через геттеры/сеттеры родительского объекта или API observe. Но если это реализовано через API observe, то у такого поведения меньше оправданий, поскольку можно было бы легко переключить флаг поддерева… Возможно, они не делают этого из соображений производительности при отслеживании глубоко вложенных структур? Но я отвлекся, это детали реализации базовых библиотек. Приведённый выше обходной путь должен помочь вам справиться с этой проблемой.

Интересно, поможет ли здесь TrackedArray?
Я не углублялся дальше этого. Просто вспомнил, что код Discourse использует это здесь и там.

import { TrackedArray, TrackedObject } from "@ember-compat/tracked-built-ins";

Представьте, если бы это было задокументировано. :laughing:

Но да, это, похоже, спроектировано именно для такого сценария.