我有一个钩子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在另一个组件中获取。
hook对数据进行转换,并使用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
,则不会发生无限循环。
我怀疑这是因为映射没有在进入钩子之前完成,所以当它完成后,它会重新触发钩子,从而触发重新渲染,在此映射会重新开始... >
这是正确的吗?对于避免无限循环我该怎么做?
答案 0 :(得分:0)
发生无限渲染是因为,当您用goalRegistrations
变换map
时,每次都会创建一个新数组并将其分配给converted
。
因此,useSingleValueChartData
钩子在每个渲染器上都会得到一个新的converted
数组,稍后将在其内部用作useEffect
钩子的第二个参数(第一个arg是回调,第二个是数组值以进行相等性比较):
}, [converted, startDate, timePeriod])
useEffect
会在每次获取新的converted
数组时看到它,并且每次都会调用它的回调,这将导致setState
进而导致重新渲染,因此无限渲染循环。
要解决此问题,您可以使用json-stable-stringify
将converted
转换为字符串,然后使用该字符串代替数组
答案 1 :(得分:0)
我通过将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]
)
那终于停止了无限循环!