Я разрабатываю один из своих проектов с помощью 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
должны запускать следующие потоки:
i-1
и повторно отрисуйте ее, но в результате этого не запускайте никаких дальнейших вычислений/рендерингов.i+1
и запустите тот же процесс для строки i+1
(шаги 1 и 2).Итак, после изменения строки i
необходимо повторно отобразить строку i-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>)
}
Можете ли вы объяснить, что такое бесконечный цикл? Поскольку прикрепленный мной код не вызывает бесконечного цикла, он обновляется столько раз, сколько необходимо, но происходит медленно.
А что касается предложения решения - скажем, я делаю это так. А потом обновляю например 5-й элемент, а следовательно 4-й и все что после 5-го. Будут ли перерисовываться в таблице только эти строки? Или все строки будут перерисованы? Вопрос для новичка, но я сейчас занимаюсь этим, когда дело доходит до React :)
Это действительно вызывает бесконечный цикл, но в конечном итоге он не сможет отправить следующий useEffect
перехватчик, как только компьютер станет настолько перегружен, что ему будут мешать проблемы с синхронизацией. Я отредактирую этот ответ, чтобы лучше описать его. Я не могу вместить все это в комментарий.
Хорошо понял. Я отредактировал исходный вопрос - посмотрите, прояснит ли это немного ситуацию и можете ли вы теперь более подробно рассказать о решении.
Хорошо, я вижу немного больше того, что вы сейчас говорите. Я еще раз отредактирую этот ответ.
Есть ли у вас это где-нибудь в репозитории, где я могу посмотреть?
Боюсь, нет, пока мы будем держать этот проект в тайне. Завтра посмотрю ваше обновление.
на самом деле это не ответ на ваш вопрос, но поскольку я действительно недавно сделал что-то подобное, у меня есть понимание, я думаю, вам следует, если возможно, изучить некоторые реактивные подходы, например, я использовал SolidJS, где у вас есть сигналы, так что
const a=createSignal<number>(1)
... тогда вы можете сделатьconst b=()=>a()*2
иb()
будет вдвое дорожеa()
. Его приятно использовать таким образом, и зависимости отслеживаются неявно, хотя вы можете указать их явно. Если вы предпочитаете что-то более близкое к реакции, есть преакт, который тоже имеет сигналы, которые могут быть полезны.