setSomeState() called from a catch() function doesn't update - reactjs

If have a problem with react code where, for some reason, everything works as expected when I update the UI from the MyApp.promise().then(<here>) but not in my MyApp.promise().then().catch(<here>)
I know the code is actually executed up to the point I actually call setData which is my useState() returned function
A call to that function in then() works just fine, not in catch()
the exception that eventually triggers catch() works fine since the catch() is executed as expected
I added a console.log() inside my component, and I see that it's no longer re-drawn when the updates comes from catch()
I guess my question is : what would be special in a catch() function so react wouldn't behave ?
This is the code for my application hook that handles upgrade status updates :
const useUpdateStatus = () => {
const [data,setData] = useState({status: STATUS.IDLE,changelog:null,tasks:[]})
const updateData = (d) => {
// We call setData with an anonymous function so we can merge previous
// data with new data
setData((prev) => {
console.log({ ...prev, ...d })
return { ...prev, ...d }
})
};
// Only once, we set the timer to periodically update status
useEffect(() => {
setInterval(() => {
MyApp.get('/system/upgrade')
.then((upgrade) => {
// If anything is not "pending", it means we are upgrading
for (var t of upgrade.tasks) {
if (t.status !== "pending") {
updateData({ status: STATUS.INSTALLING})
}
}
// updateData will call setData with the full status
// This works as intended, UI is updated on each step
updateData({ tasks: upgrade.tasks, changelog: upgrade.changelog})
})
.catch((e) => {
// If data can't be fetched, it probably means we are restarting
// services, so we updated the tasks array accordingly
setData((prev) => {
for (var t of prev.tasks) {
if (t['id'] === "restarting") {
t['status'] = 'running'
}
else if (t['status'] == "running") {
t['status'] = 'finished'
}
}
// The expected data is logged here
console.log(prev)
return prev
})
})
}, 1000);
},[])
return data
}
This is the presentation layer :
// Using the hook :
const { changelog, tasks, status } = useUpdateStatus()
// Somewhere int he page :
<UpdateProgress tasks={tasks}/>
// The actual components :
const UpdateProgress = (props) => {
return(
<div style={{display: "flex", width: "100%"}}>
{ props.tasks.map(s => {
return(
<UpdateTask key={s.name} task={s}/>
)
})}
</div>
)
}
const UpdateTask = (props) => {
const colors = {
"pending":"LightGray",
"running":"SteelBlue",
"finished":"Green",
"failed":"red"
}
return(
<div style={{ textAlign: "center", flex: "1" }}>
<Check fill={colors[props.task.status]} width="50px" height="50px"/><br/>
<p style={props.task.status==="running" ? {fontWeight: 'bold'} : { fontWeight: 'normal'}}>{props.task.name}</p>
</div>
)
}

React performs an Object.is comparison to check is a re-render is needed after a state update call. Since you are mutating the state in catch block, react is falsely notified that the state hasn't changed and hence a re-render in not triggered
You can update your state like below to make it work
.catch((e) => {
// If data can't be fetched, it probably means we are restarting
// services, so we updated the tasks array accordingly
setData((prev) => {
for (var t of prev.tasks) {
if (t['id'] === "restarting") {
t['status'] = 'running'
}
else if (t['status'] == "running") {
t['status'] = 'finished'
}
}
// The expected data is logged here
console.log(prev)
return {...prev}
})
})
However a better way to update state is to do it in an immutable manner
.catch((e) => {
// If data can't be fetched, it probably means we are restarting
// services, so we updated the tasks array accordingly
setData((prev) => ({
...prev,
tasks: prev.tasks.map((task) => {
if (task.id === "restarting") {
return { ...task, status: 'running'}
}
else if (task.id === "running") {
return { ...task, status: 'finished'}
}
return task
})
}))
})

Related

Cannot setstate in nested axios post request in react

I am trying to access the res.data.id from a nested axios.post call and assign it to 'activeId' variable. I am calling the handleSaveAll() function on a button Click event. When the button is clicked, When I console the 'res.data.Id', its returning the value properly, but when I console the 'activeId', it's returning null, which means the 'res.data.id' cannot be assigned. Does anyone have a solution? Thanks in advance
const [activeId, setActiveId] = useState(null);
useEffect(() => {}, [activeId]);
const save1 = () => {
axios.get(api1, getDefaultHeaders())
.then(() => {
const data = {item1: item1,};
axios.post(api2, data, getDefaultHeaders()).then((res) => {
setActiveId(res.data.id);
console.log(res.data.id); // result: e.g. 10
});
});
};
const save2 = () => {
console.log(activeId); // result: null
};
const handleSaveAll = () => {
save1();
save2();
console.log(activeId); // result: again its still null
};
return (
<button type='submit' onClick={handleSaveAll}>Save</button>
);
Setting the state in React acts like an async function.
Meaning that the when you set the state and put a console.log right after it, like in your example, the console.log function runs before the state has actually finished updating.
Which is why we have useEffect, a built-in React hook that activates a callback when one of it's dependencies have changed.
Example:
useEffect(() => {
console.log(activeId);
}, [activeId);
The callback will run every time the state value changes and only after it has finished changing and a render has occurred.
Edit:
Based on the discussion in the comments.
const handleSaveSections = () => {
// ... Your logic with the `setState` at the end.
}
useEffect(() => {
if (activeId === null) {
return;
}
save2(); // ( or any other function / logic you need )
}, [activeId]);
return (
<button onClick={handleSaveSections}>Click me!</button>
)
As the setState is a async task, you will not see the changes directly.
If you want to see the changes after the axios call, you can use the following code :
axios.post(api2, data, getDefaultHeaders())
.then((res) => {
setActiveId(res.data.id)
console.log(res.data.id) // result: e.g. 10
setTimeout(()=>console.log(activeId),0);
})
useEffect(() => {
}, [activeId]);
const [activeId, setActiveId] = useState(null);
const save1 = () => {
const handleSaveSections = async () => {
activeMetric &&
axios.get(api1, getDefaultHeaders()).then(res => {
if (res.data.length > 0) {
Swal.fire({
text: 'Record already exists',
icon: 'error',
});
return false;
}
else {
const data = {
item1: item1,
item2: item2
}
axios.post(api2, data, getDefaultHeaders())
.then((res) => {
setActiveId(res.data.id)
console.log(res.data.id) // result: e.g. 10
})
}
});
}
handleSaveSections()
}
const save2 = () => {
console.log(activeId); //correct result would be shown here
}
const handleSaveAll = () => {
save1();
save2();
}
return (
<button type="submit" onClick={handleSaveAll}>Save</button>
)

Trying to fix: Can't perform a React state update on an unmounted component

I have the following code:
const getAllSlidersUrl = "a url";
const adminID = "an admin id";
const adminlogintoken="a token";
const SliderContainer = () => {
const [allSlides, setAllSlides] = useState([]);
useEffect(
() => {
axios.post(
getAllSlidersUrl,
{
adminid: adminID,
token: adminlogintoken
}
)
.then(
(response) => {
setAllSlides(response.data);
}
)
.catch(
(error) => {
alert(error);
}
)
}, []
);
return (
//I have a button here
//when I press button it executes the following:
{
allSlides.map(
item =>
<div key={item.slider_id} style={{ paddingTop: "10px", paddingBottom: "10px" }}>
<SliderComponent
adminId={adminID}
adminLoginToken={adminlogintoken}
sliderID={item.slider_id}
sliderImage={item.slider_image}
sliderEnText={item.slider_en_text}
sliderArText={item.slider_ar_text}
sliderEnButtonText={item.slider_en_button_text}
sliderArButtonText={item.slider_ar_button_text}
sliderEnButtonLink={item.slider_en_button_link}
sliderArButtonLink={item.slider_ar_button_link}
deleteSliderOnClick={deleteSliderHandler}
updateSliderToAllSlides={updatingSliderHandler}
/>
</div>
)
}
);
}
When I login to the page and press F12 (to give me the inspect tab),sometimes everything is fine and I get no warning but most of the times I get the following warning:
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
and whenever this warning is present and I press the button the application breaks and I get error: allSlides.map is not a function
I tried adding return () => {} at the end of the useEffect hook, I also tried changing useEffect to the following:
useEffect(
() => {
let isSubscribed = true;
axios.post(
getAllSlidersUrl,
{
adminid: adminID,
token: adminlogintoken
}
)
.then(
(response) => {
if(isSubscribed){
setAllSlides(response.data);
}
}
)
.catch(
(error) => {
alert(error);
}
)
return () => {isSubscribed = false}
}, []
);
But in both cases where I tryied to resolve the error, it doesn't resolve it.
Note that for whatever reason (in all 3 cases) sometimes the web application doesn't break and everything works fine, but most of the times I do get the warning and then the error if I press the button.
Check the reponse data type if it is of array type or not because the error states that map is not a function that means allSlides is not an array.
About the warning
try using ref to cancel fetch like this:
const cancelFetch = React.useRef(false);
useEffect(
() => {
axios.post(
getAllSlidersUrl,
{
adminid: adminID,
token: adminlogintoken
}
)
.then(
(response) => {
if(cancelFetch.current){
return;
}
setAllSlides(response.data);
}
)
.catch(
(error) => {
alert(error);
}
)
return () => {cancelFetch.current = true;}
}, []
);
A better way to cancel is using abort controller but this can do too. If the warning is still there then maybe another component (parent) is getting unmounted.

How to use useEffect correctly to apply modifications to the array state using Firestore?

I'm using React firebase to make a Slack like chat app. I am listening to the change of the state inside the useEffect on rendering. (dependency is []).
The problem I have here is, how to fire the changes when onSnapshot listener splits out the changed state. If change.type is "modified", I use modifyCandidate (which is an interim state) to save what's been updated, and hook this state in the second useEffect.
The problem of second effect is, without dependency of chats, which is the array of chat, there is no chat in chats (which is obviously true in the initial rendering). To get chats, I add another dependency to second effect. Now, other problem I get is whenever I face changes or addition to the database, the second effect is fired even if modification didn't take place.
How can I effectively execute second effect when only modification occurs as well as being able to track the changes of chats(from the beginning) or
am I doing something awkward in the listening phase?
Please share your thoughts! (:
useEffect(() => {
const chatRef = db.collection('chat').doc('room_' + channelId).collection('messages')
chatRef.orderBy("created").onSnapshot((snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === "added") {
console.log("New message: ", change.doc.data());
}
if (change.type === "modified") {
console.log("Modified message: ", change.doc.data());
setModifyCandidate(change.doc.data());
}
if (change.type === "removed") {
console.log("remove message: ", change.doc.data());
}
});
});
}, [])
useEffect(() => {
if(!modifyCandidate){
return
}
const copied = [...chats];
const index = copied.findIndex(chat => chat.id === modifyCandidate.id)
copied[index] = modifyCandidate
setChats(copied)
}, [modifyCandidate, chats])
initially, I also use this useEffect to load chats.
useEffect(() => {
const chatRef = db.collection('chat').doc('room_' + channelId).collection('messages')
chatRef.orderBy("created").get().then((snapshot) => {
const data = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
setChats(data);
})
}, [])
return <>
{
chats.map((chat) => {
return <div key={chat.id}>
<ChatCard chat={chat} users={users} uid={uid} index={chat.id} onEmojiClick={onEmojiClick}/>
</div>
})
}
</>
use useMemo instead of 2nd useEffect.
const chat = useMemo(() => {
if(!modifyCandidate){
return null
}
const copied = [...chats];
const index = copied.findIndex(chat => chat.id === modifyCandidate.id)
copied[index] = modifyCandidate
return copied
}, [modifyCandidate])

Update mui data-grid with socket only one element added

i got a problem while working with mui data-grid and socket.io.
I load initial data from an api which works fine.
But when I want to update the table with data from an websocket is only one element added and overwritten on every change. The code is the following:
Table:
const [tableData, setTableData] = useState(props.tableData);
useEffect(() => {
setTableData(props.tableData);
}, [props.tableData]);
return (<div key="tradingTableGrid" style={{ height: 450, width: "100%" }}>
<DataGrid
key={"TradingTable"}
rows={Object.values(tableData)}
columns={tableHead}
rowHeight={30}
pageSize={10}
onRowClick={(row) => openModal(row)}
/>
</div>);
The component where the initial data are loaded and the websocket update is called is the following (i have only added the essential code):
const [data, setData] = useState({});
const [loadData, setLoadData] = useState(false);
const [socketActive, setSocketActive] = useState(false);
// initial data loading from api
const handleResponse = (response) => {
var result = {};
response.forEach((prop) => {
result = {
...result,
[prop.order_id]: {
id: prop.order_id,
orderId: prop.order_id,
price: prop.price.toString(),
volume: prop.max_amount_currency_to_trade,
minVolume: prop.min_amount_currency_to_trade,
buyVolume: prop.max_volume_currency_to_pay + " €",
minBuyVolume: prop.min_volume_currency_to_pay + " €",
user: prop.trading_partner_information.username,
},
};
});
setData(result);
};
// add from websocket
const addOrder = (order) => {
if (
order.trading_pair === "btceur" &&
order.order_type === "buy" &&
(order.payment_option === "1" || order.payment_option === "3")
) {
if (typeof data[order.order_id] === "undefined") {
console.log(data);
setData({
...data,
[order.order_id]: {
id: order.order_id,
orderId: order.order_id,
price: order.price.toString(),
volume: order.amount,
minVolume: order.min_amount,
buyVolume: Number(order.volume).toFixed(2) + " €",
minBuyVolume: Number(order.min_amount * order.price).toFixed(2) + " €",
user: "not set through socket",
},
});
}
}
};
useEffect(() => {
const loadData = () => {
setLoadData(true);
axios
.get(process.env.REACT_APP_BITCOIN_SERVICE + "/order/book?type=buy")
.then((response) => handleResponse(response.data.orders))
.catch((exception) => console.error(exception))
.finally(() => setLoadData(false));
};
if (Object.keys(data).length === 0 && socketActive === false) {
loadData();
}
const socket = () => {
const websocket = io("https://ws-mig.bitcoin.de:443/market", {
path: "/socket.io/1",
});
websocket.on("connect", function () {
setSocketActive(true);
});
websocket.on("remove_order", function (order) {
removeOrder(order);
});
websocket.on("add_order", function (order) {
addOrder(order);
});
websocket.on("disconnect", function () {
setSocketActive(false);
});
};
if (socketActive === false && Object.keys(data).length !== 0) {
socket();
}
}, [data, socketActive]);
return (
<Table
tableHeaderColor="info"
tableHead={tableHead}
tableData={data}
type="buy"
/>
);
Does anyone has an idea why on the update throught the socket only one element in the data is added and on every next call overwritten?
The Problem
The reason is that the socket handlers are using the state setters from the first render.
as an example take addOrder, you create that function within the context of a specific data value, when you call setData({...data it will always use the initial state for data. (to understand why see How do JavaScript closures work? and also A Complete Guide to useEffect from facebook's react developer Dan Abramov)
The Solution
what you can do instead is use the functional update syntax that lets you pass a function to setData:
setData( data => ({...data,
this ensures the last stored value is always used.
see the react docs on useState Functional updates

How to refactor componentWillReceiveProps to componentDidUpdate or possibly a Hook?

I have this application that has a deprecated lifecycle method:
componentWillReceiveProps(nextProps) {
if (this.state.displayErrors) {
this._validate(nextProps);
}
}
Currently, I have used the UNSAFE_ flag:
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.state.displayErrors) {
this._validate(nextProps);
}
}
I have left it like this because when I attempted to refactor it to:
componentDidUpdate(prevProps, prevState) {
if (this.state.displayErrors) {
this._validate(prevProps, prevState);
}
}
It created another bug that gave me this error:
Invariant Violation: 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.
It starts to happen when a user clicks on the PAY NOW button that kicks off the _handlePayButtonPress which also checks for validation of credit card information like so:
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.state.displayErrors) {
this._validate(nextProps);
}
}
_validate = props => {
const { cardExpireDate, cardNumber, csv, nameOnCard } = props;
const validationErrors = {
date: cardExpireDate.trim() ? "" : "Is Required",
cardNumber: cardNumber.trim() ? "" : "Is Required",
csv: csv.trim() ? "" : "Is Required",
name: nameOnCard.trim() ? "" : "Is Required"
};
if (validationErrors.csv === "" && csv.trim().length < 3) {
validationErrors.csv = "Must be 3 or 4 digits";
}
const fullErrors = {
...validationErrors,
...this.props.validationErrors
};
const isValid = Object.keys(fullErrors).reduce((acc, curr) => {
if (fullErrors[curr]) {
return false;
}
return acc;
}, true);
if (isValid) {
this.setState({ validationErrors: {} });
//register
} else {
this.setState({ validationErrors, displayErrors: true });
}
return isValid;
};
_handlePayButtonPress = () => {
const isValid = this._validate(this.props);
if (isValid) {
console.log("Good to go!");
}
if (isValid) {
this.setState({ processingPayment: true });
this.props
.submitEventRegistration()
.then(() => {
this.setState({ processingPayment: false });
//eslint-disable-next-line
this.props.navigation.navigate("PaymentConfirmation");
})
.catch(({ title, message }) => {
Alert.alert(
title,
message,
[
{
text: "OK",
onPress: () => {
this.setState({ processingPayment: false });
}
}
],
{
cancelable: false
}
);
});
} else {
alert("Please correct the errors before continuing.");
}
};
Unfortunately, I do not have enough experience with Hooks and I have failed at refactoring that deprecated lifecycle method to one that would not create trouble like it was doing with the above error. Any suggestions at a better CDU or any other ideas?
You need another check so you don't get in an infinite loop (every time you call setState you will rerender -> component did update -> update again ...)
You could do something like this:
componentDidUpdate(prevProps, prevState) {
if (this.state.displayErrors && prevProps !== this.props) {
this._validate(prevProps, prevState);
}
}
Also I think that you need to call your validate with new props and state:
this._validate(this.props, this.state);
Hope this helps.
componentDidUpdate shouldn't replace componentWillRecieveProps for this reason. The replacement React gave us was getDerivedStateFromProps which you can read about here https://medium.com/#baphemot/understanding-react-react-16-3-component-life-cycle-23129bc7a705. However, getDerivedStateFromProps is a static function so you'll have to replace all the setState lines in _validate and return an object instead.
This is how you work with prevState and hooks.
Working sample Codesandbox.io
import React, { useState, useEffect } from "react";
import "./styles.css";
const ZeroToTen = ({ value }) => {
const [myValue, setMyValue] = useState(0);
const [isValid, setIsValid] = useState(true);
const validate = value => {
var result = value >= 0 && value <= 10;
setIsValid(result);
return result;
};
useEffect(() => {
setMyValue(prevState => (validate(value) ? value : prevState));
}, [value]);
return (
<>
<span>{myValue}</span>
<p>
{isValid
? `${value} Is Valid`
: `${value} is Invalid, last good value is ${myValue}`}
</p>
</>
);
};
export default function App() {
const [value, setValue] = useState(0);
return (
<div className="App">
<button value={value} onClick={e => setValue(prevState => prevState - 1)}>
Decrement
</button>
<button value={value} onClick={e => setValue(prevState => prevState + 1)}>
Increment
</button>
<p>Current Value: {value}</p>
<ZeroToTen value={value} />
</div>
);
}
We have two components, one to increase/decrease a number and the other one to hold a number between 0 and 10.
The first component is using prevState to increment the value like this:
onClick={e => setValue(prevState => prevState - 1)}
It can increment/decrement as much as you want.
The second component is receiving its input from the first component, but it will validate the value every time it is updated and will allow values between 0 and 10.
useEffect(() => {
setMyValue(prevState => (validate(value) ? value : prevState));
}, [value]);
In this case I'm using two hooks to trigger the validation every time 'value' is updated.
If you are not familiar with hooks yet, this may be confusing, but the main idea is that with hooks you need to focus on a single property/state to validate changes.

Resources