Как я могу отслеживать состояние различных вызовов API в Redux?

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

Когда кнопка нажата, отправляется действие с именем ADD_TODO_REQUEST, которое затем будет перехвачено эпиком, наблюдаемым с редуксом, который выполнит HTTP-запрос для сохранения задачи и отправит ADD_TODO_COMPLETE или действие ADD_TODO_FAILED в зависимости от результата HTTP-запроса.

Приложение должно отображать счетчик рядом с кнопкой "Добавить задачу".
Для этого мое состояние содержит флаг с именем isSaving, который будет установлен в значение true, когда HTTP-запрос ожидает обработки, и будет сбрасывается в false после завершения HTTP-запроса.

Форма моего начального состояния выглядит так:

{
     todos: [],
     isSaving: false
}

Когда приложение запускается, первое действие, которое будет отправлено, — это FETCH_TODO_REQUEST, которое вызовет другую конечную точку API для получения всех задач.

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

Форма моего исходного состояния теперь выглядит так:

{
     todos: [],
     isSaving: false,
     isFetching: false
}

Этот подход выглядит хорошо для меня, но если пользователь также может удалять задачи, я должен отслеживать этот дополнительный статус HTTP-запроса, поэтому мне нужно добавить еще один флаг (вероятно, называемый isDeleting) в государство.

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

В сценариях, где у меня потенциально может быть много разных параллельных «действий» API, мне потребуется флаг для каждого возможного запроса.
Если бы я также хотел отображать ошибки, мне теперь потребовались бы два свойства для каждого доступного «действия» API: одно для сигнализировать о том, что запрос выполняется, а другой — удерживать объект ошибки.

Проблема с этим подходом в том, что он выглядит очень, очень, многословным.

Есть ли идиоматический и более умный способ отслеживать статус одновременных HTTP-запросов?
Правильно ли иметь флаг для каждого возможного HTTP-запроса, касающегося одного и того же «объекта»?


person Gwinn    schedule 01.01.2018    source источник
comment
Это зависит от того, как вы хотите, чтобы вещи отображались? Вы можете использовать общий флаг isDoingSomething, но тогда все компоненты, использующие этот флаг, будут одновременно отображать состояние загрузки.   -  person madebydavid    schedule 01.01.2018
comment
@madebydavid Я бы хотел иметь более одного счетчика. Например: один рядом с задачей добавления, которая видна только тогда, когда задача добавлена, и одна рядом с задачей, которая удаляется.   -  person Gwinn    schedule 01.01.2018


Ответы (2)


Я видел много способов справиться с этим, все со своими плюсами и минусами.

Каждый ресурс имеет свой статус

Хотя правил нет, обычно предлагается, чтобы пользователи Redux моделировали схему своего состояния, как если бы это была база данных.

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

{
  todosById: {
    '1': {
      id: '1',
      content: 'Do something'
    },
    '2': {
      id: '2',
      content: 'Do another thing'
    }
  }
}

Если вам нужен массив всех задач, вы создаете его на лету всякий раз, когда выбираете свое состояние:

const selectTodos = (state) => Object.values(state.todosById);
/*
  [{
    id: '1',
    content: 'Do something'
  }, {
    id: '2',
    content: 'Do another thing'
  }]
*/

const selectTodoIds = (state) => Object.keys(state.todosById)
// ['1', '2']

Зачем тебе это? Одна из причин заключается в том, что это упрощает поиск вещей по идентификатору, что часто необходимо в пользовательском потоке; например показать список вещей, а затем использовать выбирает одну из них.

Другая причина, применимая в описанной вами ситуации, заключается в том, что теперь мы можем отслеживать состояние каждого ресурса отдельно. Таким образом, каждый из них может иметь свои собственные isFetching, isSaving и т. д. Или, если вы хотите гарантировать, что ресурс находится только в одном состоянии, вы можете использовать перечисление, но будьте осторожны, что это действительно невозможно для выборки и сохранения в то же время!

{
  todosById: {
    '1': {
      id: '1',
      content: 'Do something',
      isFetching: false,
      isSaving: false
    },
    '2': {
      id: '2',
      content: 'Do another thing',
      isFetching: false,
      isSaving: false
    }
  }
}

interface Todo {
  id: string;
  content: string;
  isFetching: boolean;
  isSaving: boolean;

  // or using an enum. here are some possibilities:
  enum Status { Fetching, Saving, New, Prestine, Dirty }
  status: Status;
}

Это также работает естественным образом, когда ваш пользователь заходит непосредственно на один из ресурсов. например если они могут перейти к одному Todo, вы должны заполнить его в todosById и точно так же отслеживать его статус. Если вы позже также загрузите список задач, последнее значение с сервера будет объединено с нашей ранее загруженной задачей.

Когда вы имеете дело с созданием нового ресурса на стороне клиента, у вас, вероятно, еще не будет идентификатора для него, пока вы не сохраните его на сервере. В этом конкретном случае у меня есть временный идентификатор, который генерируется и используется только на стороне клиента. например 'tmp-todo-1', 'tmp-todo-2' и т. д.

{
  todosById: {
    'tmp-todo-1': {
      id: 'tmp-todo-1',
      content: 'A new todo that has never been saved yet',
      status: Status.New
    },
    '1': {
      id: '1',
      content: 'A todo that has been created but has unsaved changes',
      status: Status.Dirty
    },
    '2': {
      id: '2',
      content: 'Do another thing',
      status: Status.Prestine
    }
  }
}

Отдельное государство

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

Что-то похожее на это:

{
  meta: {
    '1': {
      isFetching: false,
      isSaving: false
    },
    '2': {
      isFetching: false,
      isSaving: false
    }
  },
  todosById: {
    '1': {
      id: '1',
      content: 'Do something'
    },
    '2': {
      id: '2',
      content: 'Do another thing'
    }
  }
}

Преимущество этого заключается в том, что не нужно объединять сам ресурс с состоянием только на стороне клиента, которое не включено в сервер, например, что такое isFetching.

Существует много вариантов этого, некоторые из которых создали библиотеки, такие как redux-resource, хотя я не использовал эту конкретную библиотеку, поэтому я не могу много говорить о ней. Я использую пользовательские абстракции, которые я сделал.

person jayphelps    schedule 04.01.2018
comment
Очень интересно, хотя подход по-прежнему очень многословен, он обеспечивает максимальную гибкость и детализацию. Как вы думаете, имеет ли смысл хранить глобальный флаг isFetching вместе с конкретным статусом для каждого объекта на случай, если я никогда не буду загружать их по отдельности, или вы думаете, что я должен хранить его для каждого объекта в любом случае и обновлять его для всех каждый раз, когда я их загружаю? - person Gwinn; 05.01.2018
comment
Извините, я не понимаю вопроса. - person jayphelps; 05.01.2018
comment
Извините за путаницу, мне было интересно, если сделать что-то вроде этого: /a> то есть: сохранение глобального состояния isFetching, связанного с каждым объектом, имело бы смысл в тех случаях, когда я не извлекаю каждый элемент по отдельности, а для всех других операций, специфичных для объекта, у меня есть флаги выполнения для самих объектов. . Теперь более понятно? Большое спасибо - person Gwinn; 05.01.2018

Почему бы не иметь единственное значение isBusy в магазине? затем ваши редукторы переключают это на основе ADD_TODO_.., DELETE_TO_DO_.. и т. д. Это предполагает, что вы показываете только один индикатор занятости.

person dashton    schedule 01.01.2018
comment
В этом проблема, я хочу иметь возможность показывать более одного счетчика одновременно. Например: счетчик рядом с задачей, который показывает, что задача удаляется, и счетчик рядом с кнопкой добавления задачи, который показывает, что задача добавляется. - person Gwinn; 01.01.2018