React дизайн списка с состоянием, где каждый элемент зависит от предыдущего

Я разрабатываю один из своих проектов с помощью React, и мне нужно создать список, в котором каждый элемент представляет собой строку таблицы и зависит от предыдущей строки (и частично от следующей строки) - аналогично тому, как работают Excel или Google Sheet. .
Допустим, каждый элемент имеет три поля: a, b и c. Каждый элемент обновляет свое поле a в зависимости от поля a предыдущего элемента и обновляет свое поле b в зависимости от поля c следующего элемента.

Моя текущая конструкция состоит в том, чтобы хранить каждый элемент как отдельный компонент (рендеринг строки таблицы) и хранить 2 списка в родительском компоненте — один для состояний каждого элемента и один для самих элементов.
Выглядит примерно так:

interface ItemState {
  a: number,
  b: number,
  c: number,
}

function ItemComponent({prevState, nextState, state, setState}:
        {prevState: ItemState; nextState: ItemState; state: ItemState; setState: (state: ItemState) => void;}) {

  React.useEffect(() => {
    setState({...state, a: prevState.a+1});
  }, [prevState.a]);

  React.useEffect(() => {
    setState({...state, b: nextState.c-1});
  }, [nextState.c]);

  return (<...>); // table row
}

function ParentComponent() {
  const [stateList, setStateList] = React.useState<ItemState[]>([item1, item2, item3]);
  
  const mapState = React.useCallback(
    (prev: ItemState | null, curr: ItemState, next: ItemState | null) => {
      return (
        <ItemComponent
          key = "someUniqueId"
          prevState = {prev}
          nextState = {next}
          state = {curr}
          setState = "some function which sets a specific item in the list based on id"
        />
      );
    },
    []
  );

  const itemList = React.useMemo(
    () =>
      stateList.map((state, i) => {
        const prev = i === 0 ? null : stateList[i - 1];
        const next = i === stateList.length - 1 ? null : stateList[i + 1];
        return mapState(prev, stintState, next);
      }),
    [stateList, mapState]
  );

  return (<...>); // rendered table containing the itemList
}

(Очевидно, что это упрощенная версия, на самом деле я выполняю более сложные вычисления над большим количеством полей, чем просто a, b и c.)

Это решение работает. Однако его производительность ужасна, так как обычно у меня в списке гораздо больше, чем три элемента, поэтому обновление всех умерших элементов при обновлении одного элемента занимает значительное количество времени.
Более того, когда список содержит более ~14 элементов, я начинаю получать ошибки «Превышена максимальная глубина обновления», вызванные большим количеством setState внутри useEffect.

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

Редактировать:

Основная цель здесь — отобразить таблицу, в которой я могу изменить некоторые значения в каждой строке. Изменения строки i должны запускать следующие потоки:

  1. Пересчитайте конкретные значения строки i-1 и повторно отрисуйте ее, но в результате этого не запускайте никаких дальнейших вычислений/рендерингов.
  2. Пересчитайте конкретные значения, отличные от значений на шаге 1, для строки i+1 и запустите тот же процесс для строки i+1 (шаги 1 и 2).

Итак, после изменения строки i необходимо повторно отобразить строку i-1, а также любую предыдущую строку.

Вычисления, которые я делаю, довольно сложны — отсюда и зависимость строк (в противном случае я бы напрямую вычислял данные для каждой строки при каждом обновлении, не дожидаясь предыдущего).

на самом деле это не ответ на ваш вопрос, но поскольку я действительно недавно сделал что-то подобное, у меня есть понимание, я думаю, вам следует, если возможно, изучить некоторые реактивные подходы, например, я использовал SolidJS, где у вас есть сигналы, так что const a=createSignal<number>(1)... тогда вы можете сделать const b=()=>a()*2 и b() будет вдвое дороже a(). Его приятно использовать таким образом, и зависимости отслеживаются неявно, хотя вы можете указать их явно. Если вы предпочитаете что-то более близкое к реакции, есть преакт, который тоже имеет сигналы, которые могут быть полезны.

Max 27.04.2024 20:12

Предполагается ли, что обновление значения в одной строке будет каскадно обновляться для всех строк таблицы? Или это каким-то образом ограничено строкой до и строкой после? Если намерение состоит в том, чтобы все строки были каскадными, то я думаю, что вам действительно нужен один объект в состоянии, обновления которого затем вызывают повторную визуализацию. Отдельные состояния для каждой строки не кажутся правильным способом решения этой проблемы.

Scott Z 04.05.2024 17:30

Цель состоит в том, чтобы при обновлении одной строки обновлялись все строки ниже нее (и только одна выше).

Yuval.R 04.05.2024 19:53
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
7
3
157
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Я думаю, что вы значительно усложняете это. Я думаю, что лучший подход — просто запомнить используемые вами данные в форме, которая будет соответствовать реквизитам компонента. Сделайте что-то вроде:

const formattedData = useMemo(() => doMyCalculations(stateList), [stateList])

А затем просто сопоставьте это formattedData, которое должно быть массивом объектов (или массивами, если это лучше подходит для вашего случая).

<div>
{formattedData.map((data) => {
    return <ItemComponent .../>
})}
</div>

Прямо сейчас вы запоминаете и компонент, и функцию для его создания, которые представляют собой две разные версии одного и того же.

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

Почему это вызывает бесконечный цикл

itemList, содержащий функции, генерирующие ItemComponent, зависит от части состояния stateList. В каждом ItemComponent есть useEffect хук, который запускается сразу после каждого рендеринга. В этом хуке useEffect вы устанавливаете состояние, которое изменяет часть состояния stateList. Не имеет значения, измените ли вы только один элемент в массиве, это изменит часть состояния stateList, что приведет к повторному рендерингу всех дочерних элементов, одним из которых является ItemComponent. Это, в свою очередь, снова активирует хук useEffect, который, в свою очередь, снова установит состояние, что, в свою очередь, вызовет еще один повторный рендеринг.

Если вы можете предоставить более подробную информацию о том, чего именно вы пытаетесь достичь и для чего вам нужен этот крючок useEffect, я могу дать вам более полный пример того, как вы хотели бы это сделать.

Редактировать:

Чтобы добиться чего-то, при котором строки меньше i обновляются без необходимости повторного выполнения дорогостоящих вычислений, вы можете сделать одно из двух. Либо передайте часть состояния родительскому компоненту (который у вас почти сейчас есть) и запомните данные в дочернем компоненте (которые не будут пересчитываться, даже если компонент будет повторно отображать), либо вы можете поместить Помимо запоминания данных в дочернем компоненте, вместо этого вычислите все данные в родительском компоненте, но проверьте, является ли индекс элемента индексом, который следует обновить, и если нет, используйте существующее значение. В обоих случаях потребуется повторная визуализация, но в обоих случаях вам не придется выполнять дорогостоящие вычисления для индексов, которые не следует обновлять. В первом случае вы можете столкнуться с некоторыми трудностями при попытке запустить перехватчик useMemo на основе кликнутого индекса, особенно без добавления дополнительных состояний, что, вероятно, увеличит количество рендерингов, но последний вариант будет выглядеть следующим образом:

<div>
{formattedData.map((data, i) => {
    return <ItemComponent ... index = {i} updateState = {setStateList} stateList = {stateList} />
})}
</div>

И в ItemComponent:

const ItemComponent = ({index, updateState, stateList, ...props}) => {
   const theExpensiveComputation = (item) => doStuff(item)

   const handleUpdate = () => {
        updateState(stateList.map((item, i) => i < index ? theExpensiveComputation(item) : item))
    }
   return (<div>...</div>)
}

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

Yuval.R 04.05.2024 20:05

А что касается предложения решения - скажем, я делаю это так. А потом обновляю например 5-й элемент, а следовательно 4-й и все что после 5-го. Будут ли перерисовываться в таблице только эти строки? Или все строки будут перерисованы? Вопрос для новичка, но я сейчас занимаюсь этим, когда дело доходит до React :)

Yuval.R 04.05.2024 20:42

Это действительно вызывает бесконечный цикл, но в конечном итоге он не сможет отправить следующий useEffect перехватчик, как только компьютер станет настолько перегружен, что ему будут мешать проблемы с синхронизацией. Я отредактирую этот ответ, чтобы лучше описать его. Я не могу вместить все это в комментарий.

Andrew 04.05.2024 21:30

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

Yuval.R 04.05.2024 22:38

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

Andrew 04.05.2024 22:58

Есть ли у вас это где-нибудь в репозитории, где я могу посмотреть?

Andrew 04.05.2024 23:09

Боюсь, нет, пока мы будем держать этот проект в тайне. Завтра посмотрю ваше обновление.

Yuval.R 05.05.2024 00:18

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