Error: Maximum update depth exceeded (react state) - reactjs

I am trying to use update state in a react function component but it is not working. I tried following a tutorial on pluralsite and apply it to my own project. Ideally this code should be finding the product based on the ID number and replacing the total with a new value.
Unfortunately I am getting an error when setting the state. If I switch useState(productNew) with useState(data[index]) it seems like the error doesn't appear. They seem identical in structure and I'm unsure why I'm getting this issue.
This is the error message I get in the firefox console window (the message doesn't show up in Chrome). On top of the error message, the screen appears blank:
Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops
Again, this happens when updating it with a new state, and the error does not occur when trying to set it to the original state value.
Are there any suggestions on how to solve this issue?
This is what the data looks like. In this example I am saving the first element of the array.
export let data = [
{
name: "Name",
description:
"",
products: [
{
id: 1,
name: "Name 1",
material: 1.05,
time: 25,
total: 0,
},
{
id: 2,
name: "Name 2",
material: 3,
time: 252,
total: 0,
},
],
},
...
];
function CompareCard({}) {
const index = 0;
const [productData, setProductData] = useState(data[index]);
function setTotalUpdate(id) {
const productPrevious = productData.products.find(function (rec) {
return rec.id === id;
});
const productUpdated = {
...productPrevious,
total: 1,
};
const productNew = productData.products.map(function (rec) {
return rec.id === id ? productUpdated : rec;
});
setProductData(productNew);
}
setTotalUpdate(1)
return null;
}

It looks like you are calling setState inside the body of your function component. This would cause your component to setState every time it is rendered, which then leads to another render, and another render... an infinite loop.
Instead, you should only call setState on events or within a useEffect hook.
Also, your Component needs to return some JSX, or null. It cannot return undefined.
function CompareCard({}) {
const index = 0;
const [productData, setProductData] = useState(data[index]);
function setTotalUpdate(id) {
const productPrevious = productData.products.find(function (rec) {
return rec.id === id;
});
const productUpdated = {
...productPrevious,
total: 1,
};
const productNew = productData.products.map(function (rec) {
return rec.id === id ? productUpdated : rec;
});
setProductData(productNew);
}
useEffect(() => {
// Only call setState within event handlers!
// Do not call setState inside the body of your function component
setTotalUpdate(1);
},[])
// You need to return JSX or null
return <p>productData: {JSON.stringify(productData)}</p>
}

Related

How to execute React custom hook only when data is available or Apollo client loading is false

I have custom React hook which adding some scripts and add a variable to window object:
const useMyHook = ({ id, type }: Props) => {
useScript('https:domain/main.js');
useScript('https://domain/main2.js');
useEffect(() => {
window.mydata = { id: `${id}`, type: `${type}` };
}, [id]);
};
I am using Apollo client and GraphQl for fetching data.
In my Page component, when I console.log(myData) the data returns undefined and then right after it returns the data (without refreshing). I am using Fragments.
From useQuery hook I can get the loading variable. How do I have to use loading and my custom hook so when loading === false -> use my custom hook.
I tried something like this:
const foo = useMyHook({ id: myData.id, type: myData.type });
Then below in the component in the return:
return (
{!loading && foo}
// Rest of the component jsx code
)
But still sometimes it returns first undefined?
How can I fix this issue?
# Update:
For now I added another prop loading: boolean and added this to the custom hook:
useEffect(() => {
if (!loading) {
window.mydata = { id: `${id}`, type: `${type}` };
}
}, [id]);
Is this correct approach. And why does my fragment query first returns undefined?
You can add another prop like enabled for handling this issue
sth like this:
useMyHook.tsx :
const useMyHook = ({ id, type,enabled }: Props) => {
useScript('https:domain/main.js');
useScript('https://domain/main2.js');
useEffect(() => {
if(enabled)
{
window.mydata = { id: `${id}`, type: `${type}` };
}
}, [enabled]);
};
other component:
const foo = useMyHook({ id: myData.id, type: myData.type,enabled:!loading && (data || error) });
return foo
and you need to use data and error that can be deconstructed from useQuery (like loading ) to just make sure if loading is false and data or error exists (that's because of first time that the component renders, loading is false but and data and error doesn't exists because request is not submitted yet, that's why you see undefined first )

In my React application, how can I make sure that my "useState" state is up-to-date before modifying it further?

In my React application, I have an array of objects implemented using a "useState" hook. The idea is to update an existing object within this array if an object with a specific "id" already exists or to add a new object if it does not.
The problem that I am encountering is that my state is not up-to-date before I am modifying it (= updating and/or adding objects). Therefore, the check whether an object with a certain "id" already exists fails.
Here is a (very much simplified) example demonstrating the problem:
import React, { useEffect, useState } from 'react';
interface Dummy {
id: string;
value: number;
}
const TestComponent = () => {
const [dummies, setDummies] = useState<Dummy[]>([]);
const addDummy = (id: string, value: number) => {
const dummyWithIdAlreadyUsed = dummies.find(
(dummy: Dummy) => dummy.id === id
);
if (!dummyWithIdAlreadyUsed) {
setDummies((dummies) => [...dummies, { id: id, value: value }]);
}
};
const test = () => {
addDummy('dummy1', 1);
addDummy('dummy1', 2);
addDummy('dummy2', 3);
addDummy('dummy2', 4);
addDummy('dummy3', 5);
addDummy('dummy4', 6);
addDummy('dummy4', 7);
};
useEffect(() => {
console.log(dummies);
}, [dummies]);
return <button onClick={test}>Test!</button>;
};
export default TestComponent;
The desired output is:
0: Object { id: "dummy1", value: 1 }
1: Object { id: "dummy2", value: 3 }
2: Object { id: "dummy3", value: 5 }
3: Object { id: "dummy4", value: 6 }
The actual output is:
0: Object { id: "dummy1", value: 1 }
1: Object { id: "dummy1", value: 2 }
2: Object { id: "dummy2", value: 3 }
3: Object { id: "dummy2", value: 4 }
4: Object { id: "dummy3", value: 5 }
5: Object { id: "dummy4", value: 6 }
6: Object { id: "dummy4", value: 7 }
How can I make sure that my state is up-to-date before executing setDummies?
State updates are asynchronous and queued/batched. What that means here specifically is that the dummies variable (the one declared at the component level anyway) won't have an updated state until after all 7 of these operations have completed. So that if condition is only ever checking the initial value of dummies, not the ongoing updates.
But you do have access to the ongoing updates within the callback here:
setDummies((dummies) => [...dummies, { id: id, value: value }]);
So you can expand that callback to perform the logic you're looking for:
const addDummy = (id: string, value: number) => {
setDummies((dummies) => {
const dummyWithIdAlreadyUsed = dummies.find(
(dummy: Dummy) => dummy.id === id
);
if (!dummyWithIdAlreadyUsed) {
return [...dummies, { id: id, value: value }];
} else {
return dummies;
}
});
};
In this case within the state setter callback itself you're determining if the most up-to-date version of state (as part of the queue of batched state updates) contains the value you're looking for. If it doesn't, return the new state with the new element added. If it does, return the current state unmodified.
This is a problem of closure, you can fix it by using an arrow function in the setter, check this article :
https://typeofnan.dev/why-you-cant-setstate-multiple-times-in-a-row/
Maybe you can refactor your code to avoid calling setters many times in a row by doing javascript operation before calling the setter
for exemple :
const test = (data: Dummy[]) => {
// to remove duplicate and keep the latest
const arr = data.reverse()
const latestData = arr.filter(
(item, index) =>
arr.indexOf(item) === index
);
setDummies([...dummies, ...latestDummiesData])
};

React setState componentDidUpdate React limits the number of .. when trying to update the state based on updated state

i have react program that need the data from other components. When the parent components send path data, an array will get all of the files name inside folder, and also i need to count the data length to make sure file list will not empty. Several data need to stored to state and will re-used for the next process. Below is the programs.
constructor() {
super()
this.state = {
fulldir: null,
listfileArray: [],
isDataExist: true
}
}
componentDidUpdate()
{
let path = this.props.retrievefilespath
let dirLocation = Utils.fulldirFunc(path)
let rdir = fs.readdirSync(dirLocation)
let countList = 0
rdir.forEach((filename) => {
this.state.listfileArray.push({
id: countList,
name: filename,
selected: false
})
countList++
})
if(countList > 0)
this.setState({
isDataExist: true
})
}
But this program returns error
Error Maximum update depth exceeded. This can happen when a component
repeatedly calls setState inside componentWillUpdate or
componentDidUpdate. React limits the number of nested updates to
prevent infinite loops.
Previously i used vuejs and use data to handle the data flow
data () {
return {
listfile: [],
isActive: false,
isDataExist: false,
arrayIdx: []
}
},
watch: {
lfdirHandler: function () {
//reset data from previous action
Object.assign(this.$data, getInitialData())
//electron filesystem
const fs = require('fs')
if(this.lfdirHandler.length > 0)
{
let dirLocation = Utils.fulldirFunc(this.lfdirHandler)
let rdir = fs.readdirSync(dirLocation)
var countList = 0
rdir.forEach((filename) => {
this.listfile.push({
id: countList,
name: filename,
selected: false
})
countList++
})
if(countList > 0)
this.isDataExist = true
}
},
So, what's the best solution to this problem? I mean, how to handle the data flow like what i've done with vue? is it right to use react state? Thanks.
i'm confuse with the documentation written here
componentDidUpdate(prevProps) {
// Typical usage (don't forget to compare props):
if (this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID);
}
}
after several test, i think the documentation should be written like following code:
componentDidUpdate(props) {
// Typical usage (don't forget to compare props):
// props.userID as previous data
if (this.props.userID !== props.userID) {
this.fetchData(this.props.userID);
}
}
this prevProps is confusing me.
So, the solution based on the documentation is, you need to compare, whether data inside props.userID is equal to this.props.userID.
Notes:
this.props.userID bring new data
props.userID bring old data
props.userID is saving your data from your previous action. So, if previously you setstate userID with foo. The program will save it to props.userID.
componentDidUpdate(props) {
// this.props.userID -> foo
// props.userID -> foo
if (this.props.userID !== props.userID) {
// false
// nothing will run inside here
}
}
componentDidUpdate(props) {
// this.props.userID -> foo
// props.userID -> bar
if (this.props.userID !== props.userID) {
// true
// do something
}
}
my program here:
componentDidUpdate()
{
let path = this.props.retrievefilespath
//....
}
is infinitely compare this.props.retrievefilespath with nothing. That's why the program return error.

How to return a value from Firebase to a react component? [duplicate]

This question already has answers here:
How do I return the response from an asynchronous call?
(41 answers)
Closed last year.
I am trying to read a value from RealtimeDatabase on Firebase and render it in an element, but it keeps returning undefined. I have the following code:
const getStudentName = (studentId) => {
firebase.database().ref('students').child(studentId).on("value", (snapshot) => {
return snapshot.val().name;
})
}
const StudentName = (studentId) => ( <p>{getStudentName(studentId)}</p> )
I know it's nothing wrong with the database itself or the value I'm finding, because if I do:
const getStudentName = (studentId) => {
firebase.database().ref('students').child(studentId).on("value", (snapshot) => {
console.log(snapshot.val().name);
return "Test";
})
}
I still see a correct name from my database outputted to console as expected, yet "Test" is not returned to the element. However, if I do it like this:
const getStudentName = (studentId) => {
firebase.database().ref('students').child(studentId).on("value", (snapshot) => {
console.log(snapshot.val().name);
})
return "Test";
}
then "Test" is returned to the element and displayed. I'm very confused, as I don't understand how my console.log() can be reached inside the function but a 'return' statement right after it will not return.
New to React and Firebase, please help! Thank you.
EDIT: I'm sure it's self-explanatory, but you can assume a simple database in the form:
{ "students": [
"0": { "name": "David" },
"1": { "name": "Sally" } ]}
If 'studentId' is 0 then 'console.log(snapshot.val().name)' successfully outputs 'David', but 'David' will not return to the element.
You can't return something from an asynchronous call like that. If you check in the debugger or add some logging, you'll see that your outer return "Test" runs before the console.log(snapshot.val().name) is ever called.
Instead in React you'll want to use a useState hook (or setState method) to tell React about the new value, so that it can then rerender the UI.
I recommend reading the React documentation on the using the state hook, and the documentation on setState.
I'm not sure where you are consuming getStudentName, but your current code makes attaches a real-time listener to that database location. Each time the data at that location updates, your callback function gets invoked. Because of that, returning a value from such a function doesn't make much sense.
If you instead meant to fetch the name from the database just once, you can use the once() method, which returns a Promise containing the value you are looking for.
As another small optimization, if you only need the student's name, consider fetching /students/{studentId}/name instead.
const getStudentName = (studentId) => {
return firebase.database()
.ref("students")
.child(studentId)
.child("name")
.once("value")
.then(nameSnapshot => nameSnapshot.val());
}
With the above code, getStudentName(studentId) now returns a Promise<string | null>, where null would be returned when that student doesn't exist.
getStudentName(studentId)
.then(studentName => { /* ... do something ... */ })
.catch(err => { /* ... handle errors ... */ })
If instead you were filling a <Student> component, continuing to use the on snapshot listener may be the better choice:
const Student = (props) => {
const [studentInfo, setStudentInfo] = useState({ status: "loading", data: null, error: null });
useEffect(() => {
// build reference
const studentDataRef = firebase.database()
.ref("students")
.child(props.studentId)
.child("name");
// attach listener
const listener = studentDataRef.on(
'value',
(snapshot) => {
setStudentInfo({
status: "ready",
data: snapshot.val(),
error: null
});
},
(error) => {
setStudentInfo({
status: "error",
data: null,
error
});
}
);
// detach listener in unsubscribe callback
return () => studentDataRef.off(listener);
}, [props.studentId]); // <- run above code whenever props.studentId changes
// handle the different states while the data is loading
switch (studentInfo.status) {
case "loading":
return null; // hides component, could also show a placeholder/spinner
case "error":
return (
<div class="error">
Failed to retrieve data: {studentInfo.error.message}
</div>
);
}
// render data using studentInfo.data
return (
<div id={"student-" + props.studentId}>
<img src={studentInfo.data.image} />
<span>{studentInfo.data.name}</span>
</div>
);
}
Because of how often you might end up using that above useState/useEffect combo, you could rewrite it into your own useDatabaseData hook.

Update useState immediately

useState does not update the state immediately.
I'm using react-select and I need to load the component with the (multi) options selected according to the result of the request.
For this reason, I created the state defaultOptions, to store the value of the queues constant.
It turns out that when loading the component, the values ​​are displayed only the second time.
I made a console.log in the queues and the return is different from empty.
I did the same with the defaultOptions state and the return is empty.
I created a codesandbox for better viewing.
const options = [
{
label: "Queue 1",
value: 1
},
{
label: "Queue 2",
value: 2
},
{
label: "Queue 3",
value: 3
},
{
label: "Queue 4",
value: 4
},
{
label: "Queue 5",
value: 5
}
];
const CustomSelect = (props) => <Select className="custom-select" {...props} />;
const baseUrl =
"https://my-json-server.typicode.com/wagnerfillio/api-json/posts";
const App = () => {
const userId = 1;
const initialValues = {
name: ""
};
const [user, setUser] = useState(initialValues);
const [defaultOptions, setDefaultOptions] = useState([]);
const [selectedQueue, setSelectedQueue] = useState([]);
useEffect(() => {
(async () => {
if (!userId) return;
try {
const { data } = await axios.get(`${baseUrl}/${userId}`);
setUser((prevState) => {
return { ...prevState, ...data };
});
const queues = data.queues.map((q) => ({
value: q.id,
label: q.name
}));
// Here there is a different result than emptiness
console.log(queues);
setDefaultOptions(queues);
} catch (err) {
console.log(err);
}
})();
return () => {
setUser(initialValues);
};
}, []);
// Here is an empty result
console.log(defaultOptions);
const handleChange = async (e) => {
const value = e.map((x) => x.value);
console.log(value);
setSelectedQueue(value);
};
return (
<div className="App">
Multiselect:
<CustomSelect
options={options}
defaultValue={defaultOptions}
onChange={handleChange}
isMulti
/>
</div>
);
};
export default App;
React don't update states immediately when you call setState, sometimes it can take a while. If you want to do something after setting new state you can use useEffect to determinate if state changed like this:
const [ queues, setQueues ] = useState([])
useEffect(()=>{
/* it will be called when queues did update */
},[queues] )
const someHandler = ( newValue ) => setState(newValue)
Adding to other answers:
in Class components you can add callback after you add new state such as:
this.setState(newStateObject, yourcallback)
but in function components, you can call 'callback' (not really callback, but sort of) after some value change such as
// it means this callback will be called when there is change on queue.
React.useEffect(yourCallback,[queue])
.
.
.
// you set it somewhere
setUserQueues(newQueues);
and youre good to go.
no other choice (unless you want to Promise) but React.useEffect
Closures And Async Nature of setState
What you are experiencing is a combination of closures (how values are captured within a function during a render), and the async nature of setState.
Please see this Codesandbox for working example
Consider this TestComponent
const TestComponent = (props) => {
const [count, setCount] = useState(0);
const countUp = () => {
console.log(`count before: ${count}`);
setCount((prevState) => prevState + 1);
console.log(`count after: ${count}`);
};
return (
<>
<button onClick={countUp}>Click Me</button>
<div>{count}</div>
</>
);
};
The test component is a simplified version of what you are using to illustrate closures and the async nature of setState, but the ideas can be extrapolated to your use case.
When a component is rendered, each function is created as a closure. Consider the function countUp on the first render. Since count is initialized to 0 in useState(0), replace all count instances with 0 to see what it would look like in the closure for the initial render.
const countUp = () => {
console.log(`count before: ${0}`);
setCount((0) => 0 + 1);
console.log(`count after: ${0}`);
};
Logging count before and after setting count, you can see that both logs will indicate 0 before setting count, and after "setting" count.
setCount is asynchronous which basically means: Calling setCount will let React know it needs to schedule a render, which it will then modify the state of count and update closures with the values of count on the next render.
Therefore, initial render will look as follows
const countUp = () => {
console.log(`count before: 0`);
setCount((0) => 0 + 1);
console.log(`count after: 0`);
};
when countUp is called, the function will log the value of count when that functions closure was created, and will let react know it needs to rerender, so the console will look like this
count before: 0
count after: 0
React will rerender and therefore update the value of count and recreate the closure for countUp to look as follows (substituted the value for count).This will then update any visual components with the latest value of count too to be displayed as 1
const countUp = () => {
console.log(`count before: 1`);
setCount((1) => 1 + 1);
console.log(`count after: 1`);
};
and will continue doing so on each click of the button to countUp.
Here is a snip from codeSandbox. Notice how the console has logged 0 from the intial render closure console log, yet the displayed value of count is shown as 1 after clicking once due to the asynchronous rendering of the UI.
If you wish to see the latest rendered version of the value, its best to use a useEffect to log the value, which will occur during the rendering phase of React once setState is called
useEffect(() => {
console.log(count); //this will always show the latest state in the console, since it reacts to a change in count after the asynchronous call of setState.
},[count])
You need to use a parameter inside the useEffect hook and re-render only if some changes are made. Below is an example with the count variable and the hook re-render only if the count values ​​have changed.
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
The problem is that await api.get() will return a promise so the constant data is not going to have it's data set when the line setUserQueues(queues); is run.
You should do:
api.get(`/users/${userId}`).then(data=>{
setUser((prevState) => {
return { ...prevState, ...data };
});
const queues = data.queues.map((q) => ({
value: q.id,
label: q.name,
}));
setUserQueues(queues);
console.log(queues);
console.log(userQueues);});

Resources