0
votes

I have a few hooks that fetch data using react query and do low-level calculations with that data. Another custom hook will use the output of that data to calculate a different value.

For example:

  • Hook #1 fetches a list of future expenses for a user's rental property (e.g. furnace, roof, etc.).
  • Hook #2 fetches the user's preferences, how far in the future expenses should be considered and saved for (e.g. only expenses that come due in the next 5 years are relevant).
  • Hook #3 calls hooks #1 and #2 and returns the total amount that should be set aside each month to pay for future expenses.

Since hook #3 is dependent on the data from the first two hooks, I'm not exactly sure how to structure that code. React query returns data and loading state, so I'm currently consolidating loading state from hooks #1 and #2 and putting the calculation for the data from hook #3 in a conditional. But that's resulting in re-render loop errors.

Any idea why that might be?

const usePropertyTotalMonthlyCapex = (propertyId) => {

    const [preferences, preferencesIsLoading, preferencesIsError, preferencesError] = useGetUserPreferences()
    const [expenses, isLoading, isError, error] = useGetExpenses(propertyId)

    const [capex, setCapex] = useState(0)

    if (preferences && expenses) { // this seems to be causing a render loop error
        if (expenses.length === 0) {
            setCapex(0)
        } else {
            const { outlookLength } = preferences
            const expenseValues = expenses?.map(expense => {
                const { lifespan, age, replacementCost } = expense
                if (lifespan - age > outlookLength) {
                    return 0
                }

                return replacementCost / ((lifespan - age) * 12)
            })

            setCapex(expenseValues.reduce((acc, init) => acc + init).toFixed(0))
        }

    }

    return [capex, preferencesIsLoading && isLoading]

2

2 Answers

2
votes

You don't need state for that. capex is derived state that can be computed from the state that you already have. Putting derived state into state is likely an anti-pattern. You can spot it if the setter of your useState is only called within an effect. Since computing capex is pure, you can just call it during render:

const computeCapex = (preferences, expenses) => {
  if (preferences && expenses) {
    if (expenses.length === 0) {
        return 0
    } else {
        const { outlookLength } = preferences
        const expenseValues = expenses?.map(expense => {
            const { lifespan, age, replacementCost } = expense
            if (lifespan - age > outlookLength) {
                return 0
            }

            return replacementCost / ((lifespan - age) * 12)
        })

        return expenseValues.reduce((acc, init) => acc + init).toFixed(0))
    }
  }
}

const usePropertyTotalMonthlyCapex = (propertyId) => {

    const [preferences, preferencesIsLoading, preferencesIsError, preferencesError] = useGetUserPreferences()
    const [expenses, isLoading, isError, error] = useGetExpenses(propertyId)

    const capex = computeCapex(preferences, expenses)

    return [capex, preferencesIsLoading && isLoading]
}

If the computation is expensive, you can wrap the call in useMemo, which is made for that:

const usePropertyTotalMonthlyCapex = (propertyId) => {

    const [preferences, preferencesIsLoading, preferencesIsError, preferencesError] = useGetUserPreferences()
    const [expenses, isLoading, isError, error] = useGetExpenses(propertyId)

    const capex = React.useMemo(
      () => computeCapex(preferences, expenses),
      [preferences, expenses]
    )

    return [capex, preferencesIsLoading && isLoading]
}

0
votes

Function components, and hook definitions, need to be pure functions without side effects; they cannot set state directly. Instead, you need to wrap the computation logic of your code within a useEffect or useLayoutEffect (depending on whether you want it to trigger after or before rendering); see effect hook. For example:

const usePropertyTotalMonthlyCapex = (propertyId) => {

    const [preferences, preferencesIsLoading, preferencesIsError, preferencesError] = useGetUserPreferences()
    const [expenses, isLoading, isError, error] = useGetExpenses(propertyId)

    const [capex, setCapex] = useState(0)

    useLayoutEffect(() => {
        if (preferences && expenses) {
            if (expenses.length === 0) {
                setCapex(0)
            } else {
                const { outlookLength } = preferences
                const expenseValues = expenses?.map(expense => {
                    const { lifespan, age, replacementCost } = expense
                    if (lifespan - age > outlookLength) {
                        return 0
                    }

                    return replacementCost / ((lifespan - age) * 12)
                })

                setCapex(expenseValues.reduce((acc, init) => acc + init).toFixed(0))
            }
        }
    }, [preferences, expenses])

    return [capex, preferencesIsLoading && isLoading]
}