React 自定义钩子数组依赖导致无限循环



我有一个钩子useSingleValueChartData,它接收数据数组("目标注册")并进行计算。我在不同的组件中使用钩子,如下所示:

SleepContainer.ts

import React, { FC, useContext } from 'react'
import SleepDetails from './layout'
import { SingleValueChartData } from 'models/ChartData'
import { appContext } from 'contexts/appContext'
import { GoalType, GoalWithValue } from 'models/Api/ApiGoals'
import { ApiRegistration } from 'models/Api/ApiRegistration'
import useSingleValueChartData from 'hooks/useSingleValueChartData'
import useHandleTime from 'hooks/useHandleTime'
import {
roundToPrecision,
minMaxSingleValueChartData,
minMaxValue,
} from 'components/Core/Utils/misc'
import { createGoalLineConfiguration } from 'components/Core/Utils/chartUtils'
import MinMax from 'models/MinMax'
const goalType = GoalType.Sleep
const SleepContainer: FC = () => {
/* eslint-disable @typescript-eslint/no-unused-vars */
const { startDate, endDate, timePeriod, handleTimeChange, setTimePeriod } = useHandleTime()
const { registrations, goals } = useContext(appContext)
const dirtyValues = [-1]
const goalRegistrations: ApiRegistration[] | undefined =
registrations && ((registrations[goalType] as unknown) as ApiRegistration[])
const converted: ApiRegistration[] | undefined =
goalRegistrations &&
goalRegistrations.map(reg => {
const value = reg.value || 0 / 60
return { ...reg, value }
})
const chartData: SingleValueChartData[] = useSingleValueChartData(
converted,
startDate,
endDate,
timePeriod,
dirtyValues
)
// goal lines
const goal: GoalWithValue | undefined = goals && (goals[goalType] as GoalWithValue)
const goalValue: number | null | undefined =
goal && goal.value && roundToPrecision(goal.value / 60, 1, null)
const goalLines = goalValue ? [{ ...createGoalLineConfiguration(goalValue) }] : []
// y axis domain
const dataMinMax = minMaxSingleValueChartData(chartData)
const yAxisMinMax: MinMax = minMaxValue([dataMinMax.min, dataMinMax.max, goalValue || 0], 0.1)
return (
<SleepDetails
datesVisible={{ dateFrom: startDate, dateTo: endDate }}
onTimeChange={handleTimeChange}
data={chartData}
goalValue={goalValue ? String(goalValue) : ''}
goalLines={goalLines}
yAxisMinMax={yAxisMinMax}
/>
)
}
export default SleepContainer

registrations来自上下文,并在另一个组件的早期从 API 获取。

钩子对数据进行转换,并使用useState将转换后的数据分配给内部状态。如您所见,传入的注册也在钩子依赖数组[registrations, startDate, timePeriod]中指定。

useSingleValueChartData.ts

import { useEffect, useState, useCallback } from 'react'
import moment from 'moment'
import { ApiRegistration } from 'models/Api/ApiRegistration'
import { TimePeriod } from 'models/TimePeriod'
import { SingleValueChartData, GroupedChartData, ChartDataKeys } from 'models/ChartData'
import { isBloodPressureValue, isNumberValue } from 'models/helpers'
import { getDatesBetween } from '@liva-web/core/utils/date'
import { BloodPressureValue } from 'models/Api/ApiGoals'
import { isValidRegistration, cumulativeSumArray } from 'components/Core/Utils/chartUtils'
function getBloodPressureChartData(
date: string,
regValue: BloodPressureValue
): SingleValueChartData {
const { systolic, diastolic } = regValue
const value: [number, number] = [systolic, diastolic]
return {
[ChartDataKeys.Date]: date,
[ChartDataKeys.Value]: value,
}
}
function getNumberChartData(
date: string,
value: number | null,
total: number | undefined
): SingleValueChartData {
return {
[ChartDataKeys.Date]: date,
[ChartDataKeys.Value]: value,
[ChartDataKeys.Total]: (total || 0) + (value || 0),
}
}
function initialSingleValueChartData(date: string): SingleValueChartData {
return {
[ChartDataKeys.Date]: date,
[ChartDataKeys.Value]: null,
[ChartDataKeys.Total]: 0,
[ChartDataKeys.Accumulated]: null,
}
}
export default function useSingleValueChartData<T>(
registrations: ApiRegistration<T>[] | undefined,
startDate: moment.Moment,
endDate: moment.Moment,
timePeriod: TimePeriod = TimePeriod.Week,
dirtyValues: T[] = []
): SingleValueChartData[] {
const [data, setData] = useState<SingleValueChartData[]>([])
const groupValues = useCallback(
(acc, reg) => {
if (isValidRegistration<T>(reg, startDate, endDate, dirtyValues)) {
const date = moment(reg.date).format('YYYY-MM-DD')
acc[date] = { ...reg, value: reg.value || null }
}
return acc
},
[startDate, endDate]
)
useEffect(() => {
if (registrations !== undefined) {
const groupedByDate: GroupedChartData<SingleValueChartData> = registrations.reduce(
groupValues,
{} as GroupedChartData<SingleValueChartData>
)
const allDates: string[] = getDatesBetween(startDate, endDate)
const chartData: SingleValueChartData[] = allDates.map(date => {
const { value, total } = groupedByDate[date] || {}
if (isBloodPressureValue(value)) {
return getBloodPressureChartData(date, value)
}
if (isNumberValue(value)) {
return getNumberChartData(date, value, total)
}
return initialSingleValueChartData(date)
})
const withCumulativeSum = chartData
.reduce(cumulativeSumArray, [])
// add accumulated value except for first value
// use null instead of 0 (charts are filtering null values)
.map((accumulated, i) => {
let result: number | null = null
const calculated = accumulated - (chartData[i][ChartDataKeys.Total] || 0)
if (i > 0 && calculated > 0) {
result = calculated
}
return {
...chartData[i],
[ChartDataKeys.Accumulated]: result,
}
})
setData(withCumulativeSum)
}
}, [registrations, startDate, timePeriod])
return data
}

有时(如SleepContainer)我想在将其传递给useSingleValueChartData钩子之前进行一些数据转换,因此映射将值除以 60 (const value = reg.value || 0 / 60)。

但是如果我这样做,钩子就会进入无限的重新渲染循环。如果我不进行映射,而只是按原样使用goalRegistrations,则不会发生无限循环。

我怀疑发生这种情况是因为映射在传递到钩子之前没有完成,所以当它完成时,它会重新触发钩子,这会触发重新渲染,映射重新开始......

这是对的吗?有什么想法可以避免无限循环吗?

发生无限渲染是因为当您使用map转换goalRegistrations时,每次都会创建一个新数组并将其分配给converted。 因此useSingleValueChartData钩子在每个渲染上都会获得一个新的converted数组,稍后将在其中用作useEffect钩子的第二个参数(第一个参数是回调,第二个是要比较相等的值数组):

}, [converted, startDate, timePeriod])

内部useEffect每次都会看到它,因为它每次都会得到新的converted数组,并且每次都会调用它的回调,这将导致setState进而导致重新渲染,因此渲染循环是无限的。

要解决此问题,您可以使用json-stable-stringifyconverted转换为字符串,并使用该字符串而不是数组

我通过将convertToHours()移出组件来修复(因此它不会在重新渲染时创建新函数。

function convertToHours(registrations: ApiRegistration[] | undefined): ApiRegistration[] {
return registrations ? registrations.map(reg => ({ ...reg, value: (reg.value || 0) / 60 })) : []
}
const SleepContainer: FC = () => {
...
}

我还"记住"了该值,因此只有在注册实际更改时才会重新计算它。

const converted: ApiRegistration[] | undefined = useMemo(
() => convertToHours(goalRegistrations),
[goalRegistrations]
)

这终于停止了无限循环!!

最新更新