React Custom Hook produce warning: Maximum update depth exceeded - reactjs

I'm new to React Hooks and are taking the first steps... Any help appreciated! I want to re-use logic for sorting and transforming data sets before rendering in charts. So I split it into a Custom hook but get a warning and it seems to be in a re-render loop (slowly counting up)
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
I should have a dependency array and the dependencies only change on button click.. So I don't understand why it goes into a re-render loop...?
CigarettesDetailsContainer receives "raw" data in props and passes transformed data to a child component rendering the chart. It also handles changing dates from the child so I keep that state in here.
The useSingleValueChartData hook transforms the raw data and should re-run when changes to date and time period.
CigarettesDetailsContainer
import React, { FC, useState } from 'react'
import moment from 'moment'
import { ApiRegistration } from 'models/Api/ApiRegistration'
import { CigarettesDetails } from './layout'
import { useSingleValueChartData } from 'hooks/useSingleValueChartData'
import { TimePeriod } from 'models/TimePeriod'
interface Props {
registrations: ApiRegistration[]
}
const initialStart = moment()
.year(2018)
.week(5)
.startOf('isoWeek')
const initialEnd = initialStart.clone().add(1, 'week')
const initialPeriod = TimePeriod.Week
const CigarettesDetailsContainer: FC<Props> = ({ registrations }) => {
const [startDate, setStartDate] = useState(initialStart)
const [endDate, setEndDate] = useState(initialEnd)
const [timePeriod, setTimePeriod] = useState(initialPeriod)
const data = useSingleValueChartData(
registrations,
startDate.toDate(),
endDate.toDate(),
timePeriod
)
const handleTimeChange = (change: number) => {
let newStartDate = startDate.clone()
let newEndDate = endDate.clone()
switch (timePeriod) {
default:
newStartDate.add(change, 'week')
newEndDate.add(change, 'week')
break
}
setStartDate(newStartDate)
setEndDate(newEndDate)
}
return <CigarettesDetails onTimeChange={handleTimeChange} data={data} />
}
export default CigarettesDetailsContainer
useSingleValueChartData
import React, { useEffect, useState } from 'react'
import moment from 'moment'
import { ApiRegistration } from 'models/Api/ApiRegistration'
import { TimePeriod } from 'models/TimePeriod'
import { GroupedChartData, SingleValueChartData } from 'models/ChartData'
import { createWeekdaysList } from 'components/Core/Utils/dateUtils'
export function useSingleValueChartData(
registrations: ApiRegistration[],
startDate: Date,
endDate: Date,
timePeriod: TimePeriod = TimePeriod.Week
) {
const [data, setData] = useState<SingleValueChartData[]>([])
// used for filling chart data set with days without registrations
let missingWeekDays: string[] = []
useEffect(() => {
// which days are missing data
// eslint-disable-next-line react-hooks/exhaustive-deps
missingWeekDays = createWeekdaysList(startDate)
const filteredByDates: ApiRegistration[] = registrations.filter(reg =>
moment(reg.date).isBetween(startDate, endDate)
)
const filteredByDirtyValues = filteredByDates.filter(reg => reg.value && reg.value > -1)
const grouped: SingleValueChartData[] = Object.values(
filteredByDirtyValues.reduce(groupByWeekDay, {} as GroupedChartData<
SingleValueChartData
>)
)
const filled: SingleValueChartData[] = grouped.concat(fillInMissingDays())
const sorted: SingleValueChartData[] = filled.sort(
(a: SingleValueChartData, b: SingleValueChartData) =>
new Date(a.date).getTime() - new Date(b.date).getTime()
)
setData(sorted)
}, [startDate, timePeriod])
function groupByWeekDay(
acc: GroupedChartData<SingleValueChartData>,
{ date: dateStr, value }: { date: string; value?: number }
): GroupedChartData<SingleValueChartData> {
const date: string = moment(dateStr).format('YYYY-MM-DD')
acc[date] = acc[date] || {
value: 0,
}
acc[date] = {
date,
value: value ? acc[date].value + value : acc[date].value,
}
// remove day from list of missing week days
const rest = missingWeekDays.filter(d => d !== date)
missingWeekDays = rest
return acc
}
function fillInMissingDays(): SingleValueChartData[] {
return missingWeekDays.map(date => {
return {
value: 0,
date,
}
})
}
return data
}

In the custom hook, though you want to run the effect only on change of startDate or timePeriod, at present the effect is run everytime.
This is because how startDate and endDate params are being passed to custom hook.
const data = useSingleValueChartData(
registrations,
startDate.toDate(),
endDate.toDate(),
timePeriod
)
.toDate returns new date object.
So every time new date object is being passed to custom hook.
To correct this, pass the startDate and endDate directly (i.e. without toDate) to custom hook and manage the moment to date conversion in the custom hook.

Related

How to use useEffect correctly with useContext as a dependency

I'm working on my first React project and I have the following problem.
How I want my code to work:
I add Items into an array accessible by context (context.items)
I want to run a useEffect function in a component, where the context.items are displayed, whenever the value changes
What I tried:
Listing the context (both context and context.items) as a dependency in the useEffect
this resulted in the component not updating when the values changed
Listing the context.items.length
this resulted in the component updating when the length of the array changed however, not when the values of individual items changed.
wraping the context in Object.values(context)
result was exactly what I wanted, except React is now Complaining that *The final argument passed to useEffect changed size between renders. The order and size of this array must remain constant. *
Do you know any way to fix this React warning or a different way of running useEffect on context value changing?
Well, didn't want to add code hoping it would be some simple error on my side, but even with some answers I still wasn't able to fix this, so here it is, reduced in hope of simplifying.
Context component:
const NewOrder = createContext({
orderItems: [{
itemId: "",
name: "",
amount: 0,
more:[""]
}],
addOrderItem: (newOItem: OrderItem) => {},
removeOrderItem: (oItemId: string) => {},
removeAllOrderItems: () => {},
});
export const NewOrderProvider: React.FC = (props) => {
// state
const [orderList, setOrderList] = useState<OrderItem[]>([]);
const context = {
orderItems: orderList,
addOrderItem: addOItemHandler,
removeOrderItem: removeOItemHandler,
removeAllOrderItems: removeAllOItemsHandler,
};
// handlers
function addOItemHandler(newOItem: OrderItem) {
setOrderList((prevOrderList: OrderItem[]) => {
prevOrderList.unshift(newOItem);
return prevOrderList;
});
}
function removeOItemHandler(oItemId: string) {
setOrderList((prevOrderList: OrderItem[]) => {
const itemToDeleteIndex = prevOrderList.findIndex((item: OrderItem) => item.itemId === oItemId);
console.log(itemToDeleteIndex);
prevOrderList.splice(itemToDeleteIndex, 1);
return prevOrderList;
});
}
function removeAllOItemsHandler() {
setOrderList([]);
}
return <NewOrder.Provider value={context}>{props.children}</NewOrder.Provider>;
};
export default NewOrder;
the component (a modal actually) displaying the data:
const OrderMenu: React.FC<{ isOpen: boolean; hideModal: Function }> = (
props
) => {
const NewOrderContext = useContext(NewOrder);
useEffect(() => {
if (NewOrderContext.orderItems.length > 0) {
const oItems: JSX.Element[] = [];
NewOrderContext.orderItems.forEach((item) => {
const fullItem = {
itemId:item.itemId,
name: item.name,
amount: item.amount,
more: item.more,
};
oItems.push(
<OItem item={fullItem} editItem={() => editItem(item.itemId)} key={item.itemId} />
);
});
setContent(<div>{oItems}</div>);
} else {
exit();
}
}, [NewOrderContext.orderItems.length, props.isOpen]);
some comments to the code:
it's actually done in Type Script, that involves some extra syntax
-content (and set Content)is a state which is then part of return value so some parts can be set dynamically
-exit is a function closing the modal, also why props.is Open is included
with this .length extension the modal displays changes when i remove an item from the list, however, not when I modify it not changeing the length of the orderItems,but only values of one of the objects inside of it.
as i mentioned before, i found some answers where they say i should set the dependency like this: ...Object.values(<contextVariable>) which technically works, but results in react complaining that *The final argument passed to useEffect changed size between renders. The order and size of this array must remain constant. *
the values displayed change to correct values when i close and reopen the modal, changing props.isOpen indicating that the problem lies in the context dependency
You can start by creating your app context as below, I will be using an example of a shopping cart
import * as React from "react"
const AppContext = React.createContext({
cart:[]
});
const AppContextProvider = (props) => {
const [cart,setCart] = React.useState([])
const addCartItem = (newItem)=>{
let updatedCart = [...cart];
updatedCart.push(newItem)
setCart(updatedCart)
}
return <AppContext.Provider value={{
cart
}}>{props.children}</AppContext.Provider>;
};
const useAppContext = () => React.useContext(AppContext);
export { AppContextProvider, useAppContext };
Then you consume the app context anywhere in the app as below, whenever the length of the cart changes you be notified in the shopping cart
import * as React from "react";
import { useAppContext } from "../../context/app,context";
const ShoppingCart: React.FC = () => {
const appContext = useAppContext();
React.useEffect(() => {
console.log(appContext.cart.length);
}, [appContext.cart]);
return <div>{appContext.cart.length}</div>;
};
export default ShoppingCart;
You can try passing the context variable to useEffect dependency array and inside useEffect body perform a check to see if the value is not null for example.

React Context and useCallback API refresh in child component best practice

I am using the Context API to load categories from an API. This data is needed in many components, so it's suitable to use context for this task.
The categories can be expanded in one of the child components, by using a form. I would like to be able to tell useCategoryLoader to reload once a new category gets submitted by one of the child components. What is the best practice in this scenario? I couldn't really find anything on google with the weird setup that I have.
I tried to use a state in CategoryStore, that holds a boolean refresh State which gets passed as Prop to the callback and can be modified by the child components. But this resulted in a ton of requests.
This is my custom hook useCategoryLoader.ts to load the data:
import { useCallback } from 'react'
import useAsyncLoader from '../useAsyncLoader'
import { Category } from '../types'
interface Props {
date: string
}
interface Response {
error?: Error
loading?: boolean
categories?: Array<Category>
}
const useCategoryLoader = (date : Props): Response => {
const { data: categories, error, loading } = useAsyncLoader(
// #ts-ignore
useCallback(() => {
return *APICALL with modified date*.then(data => data)
}, [date])
)
return {
error,
loading,
categories
}
}
export default useCategoryLoader
As you can see I am using useCallback to modify the API call when input changes. useAsyncloaderis basically a useEffect API call.
Now this is categoryContext.tsx:
import React, { createContext, FC } from 'react'
import { useCategoryLoader } from '../api'
import { Category } from '../types'
// ================================================================================================
const defaultCategories: Array<Category> = []
export const CategoryContext = createContext({
loading: false,
categories: defaultCategories
})
// ================================================================================================
const CategoryStore: FC = ({ children }) => {
const { loading, categories } = useCategoryLoader({date})
return (
<CategoryContext.Provider
value={{
loading,
topics
}}
>
{children}
</CategoryContext.Provider>
)
}
export default CategoryStore
I'm not sure where the variable date comes from in CategoryStore. I'm assuming that this is an incomplete attempt to force refreshes based on a timestamp? So let's complete it.
We'll add a reload property to the context.
export const CategoryContext = createContext({
loading: false,
categories: defaultCategories,
reload: () => {},
})
We'll add a state which stores a date timestamp to the CategoryStore and create a reload function which sets the date to the current timestamp, which should cause the loader to refresh its data.
const CategoryStore: FC = ({ children }) => {
const [date, setDate] = useState(Date.now().toString());
const { loading = true, categories = [] } = useCategoryLoader({ date });
const reload = useCallback(() => setDate(Date.now.toString()), []);
return (
<CategoryContext.Provider
value={{
loading,
categories,
reload
}}
>
{children}
</CategoryContext.Provider>
)
}
I think that should work. The part that I am most iffy about is how to properly memoize a function that depends on Date.now().

React Hooks, copied data keeps changing original source data

I'm creating a filtering page. I make a call to the api and then I copy the data so that with each filter requests i'm filtering from the original source. But for some reason when i'm filtering it's modifying my source. So when i filter twice its giving me wrong data. I put flags to make sure my reducer isnt' being called again.
import React, {
useEffect,
useState
} from 'react';
import {
useDispatch,
useSelector
} from "react-redux";
import {
useParams
} from "#reach/router"
import {
fetchSession
} from "../actions/dataActions";
import _ from 'lodash'
import {
useFormik
} from 'formik';
function PageFilter() {
const sessionData = useSelector(state => state.sessionData); //redux
const [filteredData, setFilteredData] = useState([]);
const formik = useFormik({
initialValues: {
date: '',
sessionType: '',
events: ''
},
onSubmit: values => {
filterData(values) //when i hit submit filter data
},
});
function filterData(values) {
let tempFilter = [];
if (values.date != '') {
sessionData.schedule_by_day.map((c, i) => {
if (c.date === values.date) {
tempFilter = [...tempFilter, c];
}
});
} else {
tempFilter = [...sessionData.schedule_by_day];
}
if (values.sessionType != '') {
tempFilter.map((c, i) => {
let tempSessionType = [];
c.session_types.map(s => {
if (parseInt(values.sessionType) == s.id) {
tempSessionType = [...tempSessionType, s]
}
});
tempFilter[i].session_types = [...tempSessionType];
//!!!!!!!
//this is a problem issue, it keeps changing my original data
//so when i call this again it's filtering from a modified SessionData
//!!!!!!!
})
} else {
}
setFilteredData([...tempFilter])
}
useEffect(() => {
dispatch(fetchSession(params.id)); //request data and updates state.sessionData
}, [])
useEffect(() => {
///when sessionData is updated filter the Data based off default form values which displays everything
if (_.isEmpty(sessionData) == false) {
filterData(formik.values)
}
}, [sessionData])
Yes, you are doing a state mutation with tempFilter[i].session_types = [...tempSessionType];. When you update any part of state you must copy it first then update the property.
It is actually rather unclear what you are really trying to do in this handler. You map a bunch of data, but don't return or save the result. It appears as though you meant to use array::forEach to issue a side-effect of pushing elements into an external array. That being said, the following is how you would/should copy the element at the specified index and create a new session_types property object reference so you are not mutating the original reference.
tempFilter[i] = {
...tempFilter[i],
session_types: [...tempSessionType],
}

Is it possible to avoid 'eslint(react-hooks/exhaustive-deps)' error on custom React Hook with useCallback?

Take the following custom React Hook to interact with IntersectionObserver:
import { useCallback, useRef, useState } from 'react';
type IntersectionObserverResult = [(node: Element | null) => void, IntersectionObserverEntry?];
function useIntersectionObserver(options: IntersectionObserverInit): IntersectionObserverResult {
const intersectionObserver = useRef<IntersectionObserver>();
const [entry, setEntry] = useState<IntersectionObserverEntry>();
const ref = useCallback(
(node) => {
if (intersectionObserver.current) {
console.log('[useInterSectionObserver] disconnect(🔴)');
intersectionObserver.current.disconnect();
}
if (node) {
intersectionObserver.current = new IntersectionObserver((entries) => {
console.log('[useInterSectionObserver] callback(🤙)');
console.log(entries[0]);
setEntry(entries[0]);
}, options);
console.log('[useInterSectionObserver] observe(🟢)');
intersectionObserver.current.observe(node);
}
},
[options.root, options.rootMargin, options.threshold]
);
return [ref, entry];
}
export { useIntersectionObserver };
ESLint is complaining about:
React Hook useCallback has a missing dependency: 'options'. Either include it or remove the dependency array.
If I replace the dependencies array with [options], ESLint no longer complains but there's now a much bigger problem, a rendering infinite loop.
What would be the right way to implement this custom React Hook without having the eslint(react-hooks/exhaustive-deps) error showing up?
The fix to this is to destructure the properties you need from options and set them in the dependancy array. That way you don't need options and the hook only gets called when those three values change.
import { useCallback, useRef, useState } from 'react';
type IntersectionObserverResult = [(node: Element | null) => void, IntersectionObserverEntry?];
function useIntersectionObserver(options: IntersectionObserverInit): IntersectionObserverResult {
const intersectionObserver = useRef<IntersectionObserver>();
const [entry, setEntry] = useState<IntersectionObserverEntry>();
const { root, rootMargin, threshold } = options;
const ref = useCallback(
(node) => {
if (intersectionObserver.current) {
console.log('[useInterSectionObserver] disconnect(🔴)');
intersectionObserver.current.disconnect();
}
if (node) {
intersectionObserver.current = new IntersectionObserver((entries) => {
console.log('[useInterSectionObserver] callback(🤙)');
console.log(entries[0]);
setEntry(entries[0]);
}, options);
console.log('[useInterSectionObserver] observe(🟢)');
intersectionObserver.current.observe(node);
}
},
[root, rootMargin, threshold]
);
return [ref, entry];
}
export { useIntersectionObserver };
You should always provide all the necessary values in the dep array to prevent it from using the previous cached function with stale values. One option to fix your situation is to memo the options object so only a new one is being passed when it's values change instead of on every re-render:
// in parent
// this passes a new obj on every re-render
const [ref, entry] = useIntersectionObserver({ root, rootMargin, threshold });
// this will only pass a new obj if the deps change
const options = useMemo(() => ({ root, rootMargin, threshold }), [root, rootMargin, threshold]);
const [ref, entry] = useIntersectionObserver(options);

React Hook useState setter for prop fails

I initialise missingWeekDays as an empty array []. I create an array of strings weekdays and assign to missingWeekDays. But when I console out weekdays has content but missingWeekDays is still empty. Why is that?
Some suggested that setMissingWeekDays should have a callback like old setState. But I can't find any documentation on this callback... A link would be appreciated!
import React, { FC, useEffect, useState } from 'react'
const Component: FC = () => {
const initialStart = moment()
.year(2018)
.week(5)
.startOf('isoWeek')
const [missingWeekDays, setMissingWeekDays] = useState<string[]>([])
const [startDate, setStartDate] = useState(initialStart)
useEffect(() => {
const weekdays = createWeekdaysList(startDate.toDate())
setMissingWeekDays(weekdays)
console.log(missingWeekDays, weekdays)
}, [startDate])
return <CigarettesDetails onTimeChange={handleTimeChange} data={data} />
}
export default CigarettesDetailsContainer
const createWeekdaysList = (startDate: Date): string[] => {
const weekdaysList: string[] = []
let weekDay = 0
while (weekDay < 7) {
const date: string = moment(startDate)
.clone()
.add(weekDay, 'day')
.format('YYYY-MM-DD')
weekdaysList.push(date)
weekDay += 1
}
return weekdaysList
}
Your setter from useState gets handled asynchronously. This results in your console log not having the updated data. This thread adds more color for you:
useState set method not reflecting change immediately

Resources