Вычисление по-прежнему остается «грязным» после оценки метода получения, при этом зависимость устанавливается асинхронно

У меня есть хранилище Pinia, которое загружает данные по запросу из базы данных (в частности, Firestore, но я не думаю, что это имеет значение).

Когда данные запрашиваются, они устанавливают значение в реактивную карту ({ id: <id>, loaded: false }). Когда запрос к базе данных возвращает результат (через обещание), он устанавливает то же значение карты для данных, загруженных из базы данных. В то же время вычисленная ссылка возвращается на исходный сайт вызова, возвращая значение базы данных, если данные присутствуют (загружено == true), или идентификатор, если нет.

Это работает, но я получил предупреждение Computed is still dirty after getter evaluation, likely because a computed is mutating its own dependency in its getter, предположительно потому, что обещание разрешается, пока вычисляется вычисляемое свойство.

Есть ли способ подавить это предупреждение, так как я хочу, чтобы вычисляемое свойство пересчитывалось при изменении данных, и это происходило максимум один раз для каждой запрашиваемой записи в базе данных.

Код моего магазина приведен ниже (машинописный текст)

export const usePlayerStore = defineStore("player", () => {
  const db = getFirestore();

  const loadedPlayers = ref(new Map<string, LoadedPlayer>());
  const subscriptions = new Map<string, Unsubscribe>();
  const loadPlayer = (playerId: string) => {
    if (!subscriptions.has(playerId)) {
      subscriptions.set(
        playerId,
        onSnapshot(doc(db, "players", playerId), (snapshot) => {
          if (snapshot.exists()) {
            const p = new LoadedPlayer(playerId, snapshot.data() as DBPlayer);
            // Warning caused by this line
            loadedPlayers.value.set(playerId, p);
          } else {
            loadedPlayers.value.delete(playerId);
          }
        }),
      );
    }
  };
  const playerName = (playerId: string) => {
    loadPlayer(playerId);
    // Warning caused by this computed property
    return computed(() => {
      const p = loadedPlayers.value.get(playerId);
      if (p) {
        return p.name;
      }
      return playerId;
    });
  };
  const getPlayer = (playerId: string): Ref<Player> => {
    loadPlayer(playerId);
    return computed(
      () =>
        (loadedPlayers.value.get(playerId) as LoadedPlayer | undefined) ?? {
          id: playerId,
          loaded: false,
          name: playerId,
        },
    );
  };

  return {
    loadPlayer,
    playerName,
    getPlayer,
  };
});

Пример использования (таблица со столбцом для каждого игрока): Написано с использованием Vue JSX.

export default defineComponent({
  props: {
    players: { type: Array as PropType<string[]>, required: true },
  },
  setup: (props) => {
    const playerStore = usePlayerStore();
    return () => <table>
      <thead>
        <tr>
          <td>&nbsp;</td>
          {props.players.map(pid => <td class = "playerName" data-player-id = {pid}>{playerStore.playerName(pid).value}</td>)}
        </tr>
      </thead>
      <tbody>
        {/* Dynamic table body content here */}
      </tbody>
    </table>;
  }
})

Пожалуйста, укажите stackoverflow.com/help/mcve вместо описания. Поскольку проблема вызвана конкретным кодом, ее необходимо решать конкретно.

Estus Flask 24.06.2024 12:26

Трудно помочь без реального кода. Также, пожалуйста, погуглите эту ошибку, возможно, она не первая с конкретной проблемой.

kissu 24.06.2024 13:06

@EstusFlask Я не публиковал код, так как это был скорее вопрос об общем решении сообщаемого состояния гонки (изменение, происходящее во время вычислительного расчета), чем конкретно об этой проблеме.

Alpvax 24.06.2024 14:45

@kissu Это был мой первый подход, но все, что я смог найти, это использование в синхронном коде.

Alpvax 24.06.2024 14:45

@Alpvax, я вижу. Это очень специфично для вашего случая. Предупреждение не является чем-то распространенным, и реальная проблема заключается в том, что вычисления используются неправильно. Является ли playerName действием? Действие не должно возвращать новую ссылку/вычисление. В большинстве случаев действия вообще не возвращают никакого результата. Я разместил ответ, это еще зависит от предполагаемого использования магазина, как его нужно переписать, это пока не показано

Estus Flask 24.06.2024 20:31
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
5
55
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вычисления используются неправильно, они должны быть определены один раз при инициализации хранилища, их не следует вызывать несколько раз с помощью return computed.

Параметризованное вычисление можно определить следующим образом:

const getPlayerName = computed(() => (playerId: string) => {...});

Но вычисление необходимо отделить от побочных эффектов, таких как loadPlayer:

loadPlayer('foo');

watchEffect(() => {
  const fooName  = getPlayerName.value('foo'),
  ...
);

Для loadPlayer также имело бы смысл вернуть обещание для строгого потока управления, это позволило бы действию вернуть обещание результата и избежать возможных состояний гонки, возникающих из-за того, что игрок не загружается в момент обращения к нему.

Спасибо. Я хотел, чтобы getPlayerName возвращал вычисленное свойство либо имени игрока, либо pid, в зависимости от того, был ли загружен игрок (когда он не загружен, имя недоступно). Таким образом, я могу использовать свойство в компонентах для отображения имени, и оно будет автоматически обновляться при возврате запроса к базе данных, без необходимости определять вычисляемое свойство в каждом компоненте (они используются как списки игроков, то есть как списки вычисляемых свойств). Не могли бы вы посоветовать лучший подход для этого, если это неправильный подход?

Alpvax 25.06.2024 12:18

loadPlayer не нужно возвращать обещание, поскольку меня на самом деле не волнует, загружен ли плеер, я просто хочу инициировать его, если он еще не загружается.

Alpvax 25.06.2024 12:20

Я ожидаю, что это будет сделано так, как предложено в ответе. Отделите loadPlayer от вычисляемого, при этом getPlayerName будет реактивным. Есть ли случай, когда этот подход вызывает проблемы?

Estus Flask 25.06.2024 12:51

Итак, если у меня есть компонент с (реактивным) списком идентификаторов игроков, и я хочу отображать их как имена, когда они доступны, у меня должна быть реактивная вся функция getName, а не ComputedRef[]? Как мне гарантировать, что игроки загружаются? Должен ли я иметь в списке отдельный наблюдатель и вызывать store.loadPlayer для каждого?

Alpvax 25.06.2024 15:08

Моя цель состояла в том, чтобы все это работало как компоновка, где я мог бы просто предоставить сайту вызова единственный метод для получения имени игрока и загрузки в фоновом режиме. Должно ли это быть выделено в отдельный составной объект с наблюдателем внутри него, который сам ссылается на хранилище?

Alpvax 25.06.2024 15:12

Можете ли вы предоставить упрощенную схему, которая использует эти данные и отражает ваше реальное использование, чтобы ее можно было конкретно рассмотреть? То, как это должно работать, зависит от того, что происходит с getPlayerName. Обычно у вас есть {{ getPlayerName('foo') }} в шаблоне и loadPlayer('foo') в части сценария (вероятно, внутри наблюдателя, если 'foo' является динамическим значением), и это будет работать. Я совершенно уверен, что в целом существует более одного способа добиться этого, но что касается части Pinia, то она должна быть довольно строгой, есть «геттеры» и «действия».

Estus Flask 25.06.2024 15:33

Я добавил в ОП образец упрощенного компонента. Информация об игроках может не поступать из базы данных, поэтому нет гарантии, что игроки с указанными идентификаторами будут загружены. Было бы неплохо иметь возможность запрашивать имена с помощью простой функции, но я не уверен на 100%, как бы работала реактивность, если бы это была просто функция, а не возврат вычисленной ссылки. Я сильно усложняю ситуацию?

Alpvax 25.06.2024 17:56

Я ожидаю, что это будет watchEffect(() => { props.players.forEach(pid => store.loadPlayer(pid)) }) в настройке компа и {store.getPlayerName(pid)} в функции рендеринга. Реактивность обеспечивается тем фактом, что доступ к loadedPlayers.value.get() осуществляется внутри getPlayerName при запуске функции рендеринга. Вычисление само по себе не улучшит реактивность, оно полезно для оптимизации, но не в этом случае. getPlayerName на самом деле может быть обычной функцией, которая возвращает строку, она не имеет особой пользы от вычисления(), это всего лишь соглашение, которое предотвращает ее переназначение в хранилище

Estus Flask 25.06.2024 18:23

Другие вопросы по теме