反应自定义挂钩数组依赖导致无限循环

时间:2019-10-29 16:33:23

标签: reactjs react-hooks

我有一个钩子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,则不会发生无限循环。

我怀疑这是因为映射没有在进入钩子之前完成,所以当它完成后,它会重新触发钩子,从而触发重新渲染,在此映射会重新开始...

这是正确的吗?对于避免无限循环我该怎么做?

2 个答案:

答案 0 :(得分:0)

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

}, [converted, startDate, timePeriod])

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

要解决此问题,您可以使用json-stable-stringifyconverted转换为字符串,然后使用该字符串代替数组

答案 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]
)

那终于停止了无限循环!