Изменить внутреннее состояние компонента после изменения избыточного хранилища, но до визуализации компонента - PullRequest
0 голосов
/ 01 октября 2019

Допустим, у меня есть компонент реагирования, который подключен к хранилищу резервов, но также управляет некоторым внутренним состоянием. Внутреннее состояние содержит data для извлечения и loading флаг, который используется для визуализации либо текста «Загрузка ...», либо данных после его извлечения. Мне также нужно selectedItems, что я получаю из магазина редуксов. Эти элементы также используются в других частях приложения, например, на боковой панели, показывающей выбранные в данный момент элементы, поэтому пользователь всегда знает, какие элементы выбраны, и может использовать боковую панель для выбора или удаления элементов.

В этом компонентеselectedItems используются для сопоставления данных, которые я получаю.

Итак, компонент выглядит примерно так:

export default () => {
  const selectedItems = useSelector(state => state.itemsReducer.selectedItems);

  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchData('someApi/data').then(res => {
      setData(res);
      setLoading(false);
    });
  }, [selectedItems]);

  return (
    <div>
      {loading ? (<div>Loading...</div>) : (<div>{data}</div>)}
    </div>
  );
}

Теперь давайте скажем, что в части <div>{data}</div> я на самом делесделать некоторое сложное сопоставление, где я использую информацию из selectedItems для правильного отображения данных.

У меня проблема в следующем: если пользователь добавляет элемент через боковую панель, которая затем меняет selectedItems всохранить, это означает, что текущий data больше не действителен, и мне нужно получить новые данные. Поэтому сразу после смены хранилища я бы хотел установить loading на true, чтобы отображался текст «Загрузка ...», а затем получать новые данные после монтирования компонента, поэтому в useEffect. Итак, я знаю, что useEffect вызывается только после рендеринга компонента. Тем не менее, я сопоставляю data в JSX, который использует selectedItems, который уже был обновлен на основе изменения хранилища, но data все еще старый, так как useEffect еще не был вызван. Я вообще не хочу отображать data в течение времени между изменением selectedItems и получением новых данных. Итак, я в основном хочу, чтобы loading было установлено в значение true между этими двумя событиями, но как мне это сделать? Задать его в useEffect слишком поздно, так как компонент уже отрендерен / обновлен, но до этого он уже знал об изменении в selectedItems (что фактически вызывает ошибку, когда он пытается получить доступ к некоторым свойствам в data основанный на новом выбранном элементе, но data все еще старый, поэтому он не содержит его.

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

ОБНОВЛЕНИЕ на основе ответа @ brendangannon

Так что я мог бы привести слишком упрощенный пример. Отображение Я имел в виду отображение данных в JSX. Сначала я отображаю на data, и у него есть вложенная карта на selectedItems, он создает структуру в виде таблицы, где строки основаны на data, и кроме первого столбца,количество столбцов основано на selectedItems.

Одна вещь, которую я не упомянул, но сейчас кажется уместной, заключается в том, что на самом деле data попадает в компонент с помощью другого хука.

Итак, вместо useEffect выше, у меня есть что-то вроде этого:

export default () => {
  // ...
  const {data, loading} = useLoadData(...someParams);
  // ...
}

Хук сохраняет внутреннее состояние и извлекает данные в свой собственный useEffect, который зависит отselectedItems и извлекает новые данные в случае изменения selectedItems на основе этих элементов.

Имеет ли смысл сохранять внутреннее состояние также в моем компоненте, который будет копией data, давайте назовемэто dataToMap на данный момент, что будет использоваться в JSX? И я бы также добавил useEffect с зависимостью [data], который бы устанавливал состояние для этого компонента при загрузке новых данных, в основном обновляя dataToMap. Таким образом, если selectedItems изменилось, этот компонент будет повторно визуализироваться при использовании устаревшего dataToMap, а затем внутренний useEffect из useLoadData начнет загружать новые данные внутренне, давая мне loading === true, который в следующемrender будет использовать для отображения текста «Загрузка ...», а затем при получении свежего data мой хук срабатывает из-за изменения зависимости [data], устанавливает новое dataToMap во внутреннее состояние, что вызывает еще один повторный рендеркоторые могут (и будут, благодаря loading===false, установленному внутри useLoadData) теперь безопасно отображать новые данные в состоянии.

Таким образом, компонент будет выглядеть примерно так:

export default () => {
  // ...
  const selectedItems = useSelector(state => state.itemsReducer.selectedItems);

  const {data, loading} = useLoadData(...someParams);

  const [dataToMap, setDataToMap] = useState([]);

  useEffect(() => {
    if (loading === false) {
      // new data fetched
      setDataToMap(data);
    }
  }, [data]);

  return (
    <div>
      {loading ? (<div>Loading...</div>) : (
        <div>
         {dataToMap.map(dataEl => {
           return (
             <>
             // render some row stuff
             {selectedItems.map(selItem => {
               // render some column stuff
             })}
             </>
           )
         })}
        </div>
      )}
    </div>
  );
}

Это оставляет меня с двумя вопросами:

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

ОБНОВЛЕНИЕ 2

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

Я думаю, я просто сохраню selectedItems внутри себя в состоянии useLoadData и верну его моему компоненту для отображения, например, selectedItemsForMapping. Когда selectedBuckets изменится, он все равно будет использовать старый selectedBucketsForMapping для рендеринга поверх устаревших данных, и после этого он вернет новое selectedItemsForMapping вместе с loading===true, чтобы я мог показать «Загрузка ...» иостальное уже обсуждалось выше.

1 Ответ

1 голос
/ 01 октября 2019

Я думаю, вы захотите немного изменить свой дизайн. «Сложное отображение» не должно (как правило) происходить при рендеринге - вы не захотите повторять это отображение при каждом рендеринге независимо от того, как вы управляете состоянием. Лучше всего рассчитывать эти производные данные в точке, где исходные данные изменяются (в данном случае это результат вызова API), и иметь данные, готовые к использованию в момент, когда они «входят» в компонент (в данном случае, когдаустановлено состояние). Хранение обработки данных отдельно от рендеринга является хорошим примером отдельных / отдельных обязанностей и облегчает тестирование.

Так что это может выглядеть примерно так:

export default () => {
  const selectedItems = useSelector(state => state.itemsReducer.selectedItems);

  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchData('someApi/data').then(res => {
      const modifiedData = doComplexMapping(res, selectedItems);
      setData(modifiedData);
      setLoading(false);
    });
  }, [selectedItems]);

  return (
    <div>
      {loading ? (<div>Loading...</div>) : (<div>{data}</div>)}
    </div>
  );
}

В этом случае все render делает визуализацию чего-либо из состояния, поэтому оно может безопасно выполняться, пока в состоянии есть данные, независимо от того, устарели они или нет. Когда selectedItems изменится в хранилище, компонент снова выполнит рендеринг (с указанием старых данных), затем сразу установит состояние «загрузки», отобразит пользовательский интерфейс загрузки, а затем получит новые данные, обработает их, установит их в состояниеи визуализировать новые данные. Первые два рендеринга будут происходить достаточно быстро, чтобы вы, вероятно, не заметили повторный рендеринг устаревших данных;он покажет пользовательский интерфейс «загрузки» более или менее немедленно.

Однако, поскольку любое изменение selectedItems инициирует новый вызов API, может иметь смысл переместить этот вызов API из этого компонента всоздатель действия, который меняет selectedItems. Независимо от того, какое сложное отображение происходит в данный момент в этом компоненте, оно также будет передано создателю действия, и производные данные, которые вы в настоящее время генерируете в этом компоненте, вместо этого будут установлены в хранилище избыточных данных, и этот компонент извлечет его через селектор.

Таким образом, изменение состояния приложения (selectedItems изменилось из-за взаимодействия с пользователем) напрямую связано с повторным извлечением данных (через вызов API), а не косвенно, через побочный эффект компонентасделать вниз по течению изменения состояния. Изменяя производные данные в том же месте, где происходит первоначальное изменение состояния, код более четко отражает причинно-следственную природу приложения. Это зависит от дизайна приложения в целом, но в приложениях реагирования / редукции выборка данных обычно происходит таким образом на уровне создателя действий, особенно когда есть несколько компонентов пользовательского интерфейса, вовлеченных в изменение и рендеринг связанного состояния. .

...