useCallback with updated state object - React.js - javascript

I have a POST API call that I make on a button click. We have one large state object that gets sent as body for a POST call. This state object keeps getting updated based on different user interactions on the page.
function QuotePreview(props) {
const [quoteDetails, setQuoteDetails] = useState({});
const [loadingCreateQuote, setLoadingCreateQuote] = useState(false);
useEffect(() => {
if(apiResponse?.content?.quotePreview?.quoteDetails) {
setQuoteDetails(apiResponse?.content?.quotePreview?.quoteDetails);
}
}, [apiResponse]);
const onGridUpdate = (data) => {
let subTotal = data.reduce((subTotal, {extendedPrice}) => subTotal + extendedPrice, 0);
subTotal = Math.round((subTotal + Number.EPSILON) * 100) / 100
setQuoteDetails((previousQuoteDetails) => ({
...previousQuoteDetails,
subTotal: subTotal,
Currency: currencySymbol,
items: data,
}));
};
const createQuote = async () => {
try {
setLoadingCreateQuote(true);
const result = await usPost(componentProp.quickQuoteEndpoint, quoteDetails);
if (result.data?.content) {
/** TODO: next steps with quoteId & confirmationId */
console.log(result.data.content);
}
return result.data;
} catch( error ) {
return error;
} finally {
setLoadingCreateQuote(false);
}
};
const handleQuickQuote = useCallback(createQuote, [quoteDetails, loadingCreateQuote]);
const handleQuickQuoteWithoutDeals = (e) => {
e.preventDefault();
// remove deal if present
if (quoteDetails.hasOwnProperty("deal")) {
delete quoteDetails.deal;
}
handleQuickQuote();
}
const generalInfoChange = (generalInformation) =>{
setQuoteDetails((previousQuoteDetails) => (
{
...previousQuoteDetails,
tier: generalInformation.tier,
}
));
}
const endUserInfoChange = (endUserlInformation) =>{
setQuoteDetails((previousQuoteDetails) => (
{
...previousQuoteDetails,
endUser: endUserlInformation,
}
));
}
return (
<div className="cmp-quote-preview">
{/* child components [handleQuickQuote will be passed down] */}
</div>
);
}
when the handleQuickQuoteWithoutDeals function gets called, I am deleting a key from the object. But I would like to immediately call the API with the updated object. I am deleting the deal key directly here, but if I do it in an immutable way, the following API call is not considering the updated object but the previous one.
The only way I found around this was to introduce a new state and update it on click and then make use of the useEffect hook to track this state to make the API call when it changes. With this approach, it works in a weird way where it keeps calling the API on initial load as well and other weird behavior.
Is there a cleaner way to do this?

It's not clear how any children would call the handleQuickQuote callback, but if you are needing to close over in callback scope a "copy" of the quoteDetails details then I suggest the following small refactor to allow this parent component to use the raw createQuote function while children receive a memoized callback with the current quoteDetails enclosed.
Consume quoteDetails as an argument:
const createQuote = async (quoteDetails) => {
try {
setLoadingCreateQuote(true);
const result = await usPost(componentProp.quickQuoteEndpoint, quoteDetails);
if (result.data?.content) {
/** TODO: next steps with quoteId & confirmationId */
console.log(result.data.content);
}
return result.data;
} catch( error ) {
return error;
} finally {
setLoadingCreateQuote(false);
}
};
Memoize an "anonymous" callback that passes in the quoteDetails value:
const handleQuickQuote = useCallback(
() => createQuote(quoteDetails),
[quoteDetails]
);
Create a shallow copy of quoteDetails, delete the property, and call createQuote:
const handleQuickQuoteWithoutDeals = (e) => {
e.preventDefault();
const quoteDetailsCopy = { ...quoteDetails };
// remove deal if present
if (quoteDetailsCopy.hasOwnProperty("deal")) {
delete quoteDetailsCopy.deal;
}
createQuote(quoteDetailsCopy);
}

Related

State values passed in as arguments in useEffect not being read in reactjs

I am trying to build a cryptocurrency application. But I am having trouble getting populated state values that are passed into the useEffect as parameters to a debouncing/polling function.
The issue is that the debouncing works well, as in it detects the value and calls the api after the 500ms that I specified in debounce. However, the polling portion seems to not have the state values of of transactionType, fromCurrencyAmount, and pair. It seems like after I debounce the input, after 6 seconds the polling will do its thing but the values passed in the params are undefined. Is there anyway I can solve this?
Here is the method that serves two purposes. It has an api to be polled from every 6 seconds, as well as getting debounced input if the user enters an amount inside the input.
function handleInitPoll(baseAndQuote, side, value) {
getSwapPrice(baseAndQuote, side, value || 0)
.then((res) => {
if (!res.price) {
setIsLoading(true);
} else if (res.error) {
setErrorMessage(res.error);
} else if (res.price) {
setIsLoading(false);
setSwapPriceInfo(res);
}
});
}
And here is the useEffect:
useEffect(() => {
handleInitPoll(pair, transactionType, fromCurrencyAmount);
const timer = setInterval(handleInitPoll, 6000, pair, transactionType, fromCurrencyAmount);
return () => {
clearInterval(timer);
};
}
setSelectedCurrencyState({ ...selectedCurrencyState, selectedFromCurrency: 'USDT', selectedToCurrency: 'XLM' });
}, [pair, transactionType, fromCurrencyAmount]);
And here is the debounce declaration:
const debounceOnChange = useCallback(debounce(handleInitPoll, 500, pair, transactionType, fromCurrencyAmount), []);
And here is where the debouncing is being done, which is inside an onChange handler:
const handleAssetAmount = (e) => {
const { value } = e.target;
const formattedAmount = handleAssetAmountFormat(value);
setFromCurrencyAmount(formattedAmount);
validateInputAmount(formattedAmount);
debounceOnChange(pair, transactionType, formattedAmount);
};
Issue
The issue here is that you've closed over stale values in the interval callback.
Solution
One solution is to cache these state values in a React ref such that the current value can be accessed in the polling function.
Example:
const pairRef = React.useRef(pair);
const transactionTypeRef = React.useRef(transactionType);
const fromCurrencyAmountRef = React.useRef(fromCurrencyAmount);
useEffect(() => {
pairRef.current = pair;
}, [pair]);
useEffect(() => {
transactionTypeRef.current = transactionTypeRef;
}, [transactionType]);
useEffect(() => {
fromCurrencyAmountRef.current = fromCurrencyAmount;
}, [fromCurrencyAmount]);
useEffect(() => {
handleInitPoll(pair, transactionType, fromCurrencyAmount);
const timer = setInterval(() => {
handleInitPoll(
pairRef.current,
transactionTypeRef.current,
fromCurrencyAmountRef.current
);
}, 6000);
return () => {
clearInterval(timer);
};
}, [pair, transactionType, fromCurrencyAmount]);
Fundamentally, your code seems to be correct with a few issues:
There is a race condition.
If getSwapPrice is running and the component is updated, it can still
affect the state when setSwapPriceInfo or setLoading are called when
the promise is resolved.
This is particularly bad, because network requests can "overtake" each
other. Thus it can happen that the return value of getSwapPrice
updates the component with the result of an old network request.
This is discussed in this article.
There is this odd call to setSelectedCurrencyState in the useEffect
block. It's not clear what this is supposed to do, but it clearly doesn't
belong there.
However, the underlying application should work fine, I reproduced it with a simpler application here:
import { useEffect, useState } from "react";
function fetchExchangeRateAsync(multiplier) {
return new Promise(resolve => {
setTimeout(() => {
resolve(Math.random() * multiplier);
}, 100);
});
}
function CurrencyExchangeRate() {
const [exchangeRate, setExchangeRate] = useState(null);
const [inputValueString, setInputValueString] = useState("");
const inputValue = Number(inputValueString);
const [multiplier, setMultiplier] = useState(1.0);
let outputValue = null;
if (!isNaN(inputValue) && exchangeRate !== null) {
outputValue = inputValue * exchangeRate;
}
useEffect(() => {
// To avoid race conditions, we must not update the state from an asynchronous operation if
// the component was re-rendered since then.
//
// https://overreacted.io/a-complete-guide-to-useeffect/
let didCancel = false;
// Do not delay the first request.
fetchExchangeRateAsync(multiplier)
.then(newExchangeRate => {
if (!didCancel) {
setExchangeRate(newExchangeRate);
}
});
// Poll exchange rate.
let intervalHandle = setInterval(() => {
fetchExchangeRateAsync(multiplier)
.then(newExchangeRate => {
if (!didCancel) {
setExchangeRate(newExchangeRate);
}
});
}, 500);
return () => {
didCancel = true;
clearInterval(intervalHandle);
};
}, [multiplier]);
return (
<div>
<input value={inputValueString} onChange={event => setInputValueString(event.target.value)} /><br />
<p>With current exchange rate: {outputValue !== null ? outputValue : "(loading)"}</p>
<button onClick={() => setMultiplier(100.0)}>Set Multiplier</button>
</div>
);
}
function App() {
return (
<CurrencyExchangeRate />
);
}
export default App;
This is quite a bit different from what you are doing but it does demonstrate that your code should generally work:
It is possible to trigger the fetch logic by changing the input field (here without debouncing) or in a given interval.
The fetch logic runs immediately when the component is rendered for the first time.
The "Set Multiplier" button can affect the value of multiplier and this information arrives in the setInterval call correctly.
This works because [multiplier] dictates that the effect should be re-run if that variable changes. When this happens, the old interval is first cleared with clearInterval and then re-started with setInterval.
In your case that would be pair, transactionType and fromCurrencyAmount instead of multiplier.
In other words, your issue seems to be outside the code that you provided in the question.

How to fetch up-to-date data inside async?

I have a function which fetches data.
const fetchData = async (filter) => {
if (loading) return
loading = true
const data = await api(filter)
setData(data)
loading = false
}
I also have a filter component, when I change filters it calls my fetchData() function with the new filter variable.
This all works, however there is an issue
This issue occurs when I switch my filter but my fetching function is in a loading state. This causes the if check to fail and I now see outdated data because a re-fetch never happens.
My initial idea was to create a const q = [] variable, and inside if (loading) i would push my filters, and somehow at the end I would re-fetch with the last element inside my q array and then clear that array.
I dont really know how to do that re-fetch logic. A setInterval(() => checkQ(), 1000)? Doesn't seem right
What would be a better approach to take?
You should use an AbortController - that's part of the fetch, as my experience tells me that it's not hard to initiate a new fetch request, but what to do with the first request, when you send out a second?
Here's a snippet that will do the thing you asked - but also deals with the unnecessary requests:
const { useState, useEffect } = React
const useFetchData = () => {
const [users, setUsers] = useState([])
let controller = null
const fetchData = () => {
console.log('fetch initiated')
if (controller) controller.abort()
controller = new AbortController();
const { signal } = controller;
fetch('https://jsonplaceholder.typicode.com/users', { signal })
.then(response => {
console.log('request response')
return response.json()
})
.then(json => {
console.log('retrieved list:', json)
setUsers(() => json || [])
})
.catch(err => {
if(err.name === "AbortError") {
console.warn('Abort error', err)
}
})
}
return { fetchData }
}
const FetchData = () => {
const { fetchData } = useFetchData()
return (
<div>
<button onClick={fetchData}>FETCH DATA</button><br />
</div>
)
}
const FetchAbortFetchData = () => {
const { fetchData } = useFetchData()
return (
<div>
<button onClick={() => {
fetchData()
fetchData()
}}>FETCH-ABORT-FETCH DATA</button><br />
</div>
)
}
const App = () => {
return (
<div>
<FetchData /><br />
<FetchAbortFetchData />
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
<script src="https://unpkg.com/react#17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
Easiest way is to use your filter criteria as the lock.
Advantages
Always fetching data immediately
Only calling setData with the results from the most recent filter criteria
Simple
Disadvantages
May have multiple simultaneous requests
Toggling back/forth between filters may lead to a race condition.
let latestFilter = null;
const fetchData = async (filter) => {
// Check to see if its there's the same request already in progress
// Note: may need to deep equal to check the filter
if (filter === latestFilter) return;
// Set the current request as most up to date
latestFilter = filter
// Fetch Data (async)
const data = await api(filter)
// If the filter is still the most up to date, use it. Otherwise discard.
// Note: may need to deep equal to check the filter
if (filter === latestFilter) {
setData(data)
latestFilter = null
}
}
In order to solve disadvantage 2, you can include a counter. This will ensure that only the most recent request will run setData.
let latestFilter = null;
let latestRequestNumber = 0
const fetchData = async (filter) => {
if (filter === latestFilter) return;
latestFilter = filter
// Identify the current request
const requestNumber = latestRequestNumber + 1;
// Store this number
latestRequestNumber = requestNumber
const data = await api(filter)
// Update data only if we are the most recent request.
if (callCount = latestCallCount) {
setData(data)
latestFilter = null
}
}

React-hook setState does not work as expected

I have the following code:
const classes = useStyles();
const [data1, setData1] = useState([]);
const [searchedString, setSearchString] = useState("");
console.log(data1);
const fetchDataHandler = async () => {
setData1([]);
axios
.get(`http://localhost:5000/select?articul=${searchedString}`)
.then((response) => {
dataStruction(response.data);
})
.catch((err) => {
console.log(err);
});
};
const dataStruction = (data) => {
data.map((element1) => {
if (element1.secondaryArt.startsWith("30")) {
return setData1([...data1, { ...element1, level: 1 }]);
}
});
};
const onChangeSearchText = (event) => {
setSearchString(event.target.value);
};
I want whenever I call fetchDataHandler to be able to set data1 to empty array. Now it is working as that results are sticking every time I call fetchDataHandler. How can I do it?
Problem:
Your Asynchronous handler dataStruction closes over data1 before a new render is triggered at setData1([]); (at the top of your async function).
This happens because React state updates are batched and asynchronous.
Simple Solution:
If you get rid of (delete the line) setData1([]); and change setData1([...data1, { ...element1, level: 1 }]); to setData1([{ ...element1, level: 1 }]); then you will get an array with a new element in it without preserving the "old" elements.
Alternative Solution:
You can also wrap your state updates into functions like so:
Turn this: setState("foo")
Into this: setState((state, props) => "foo")
The second form (passing a function instead directly a state) ensures that the correct state is referenced. So in your case the second state update would reference the updated state.

Will there be an issue when this.setState is used inside of .then?

While I was working on a project, there some case that I have to change a state when user update a data.
Here is the code :
state = {
changedReservationStatus: toJS(this.props.reservationViewModel.getReservation())
}
reloadPageData = async () => {
const { reservationViewModel, reservationOrderViewModel } = this.props
const reservation = toJS(reservationViewModel.getReservation())
await reservationViewModel.fetchReservation(reservation.id).then(changedReservationStatus => this.setState({ changedReservationStatus }))
}
( reservation variable contains a data of an array. )
When user clicks save button, reloadPageData function works and then await reservationViewModel.fetchReservation starts to work, it triggers this.setState after .then statement.
But as you may know reservationViewModel.fetchReservation and this.setState both work asynchronously.
when I console.log this.state.changedReservationStatus inside render function, it renders the data I want to get.
But is it okay to use this.setState inside of Promise?
Will there be any issue?
Try the following solution .
state = {
changedReservationStatus: toJS(this.props.reservationViewModel.getReservation())
}
reloadPageData = async () => {
const { reservationViewModel, reservationOrderViewModel } = this.props
const reservation = toJS(reservationViewModel.getReservation())
await changedReservationStatus = reservationViewModel.fetchReservation(reservation.id);
this.setState({ changedReservationStatus });
}

Can i set the state here? React-Native

In the getMatches function, I want to set the state, with the matches associated with the leagueID. Where I am running into problems is how to setState in the get Matches function.The fetch response gives me exactly what I want back. Its an array with matches associated with the LeagueID. That setState will run twice because I am using 2 leagueIds. Any help or advice would be greatly appreciated.
export default class MatchView extends Component {
state = {
matches: null,
leagueName: null
}
componentWillMount = () => {
this.getLeagueNames();
}//end componentwwillMOunt
// componentDidMount = () => {
// console.log("league_array", this.state.leagueName)
// }
getLeagueNames = () => {
let leagueArray = [];
fetch(`https://apifootball.com/api/?action=get_leagues&APIkey=42f53c25607596901bc6726d6d83c3ebf7376068ff89181d25a1bba477149480`)
.then(res => res.json())
.then(leagues => {
leagues.map(function(league, id){
leagueArray = [...leagueArray, league]
})//end .map
this.setState({
leagueName: leagueArray
});
})
}// end getLeagueNames
getMatches = (leagueID) => {
let LeagGamesArray = [];
console.log('working')
fetch(`https://apifootball.com/api/?action=get_events&from=2016-10-30&to=2016-11-01&league_id=${leagueID}&APIkey=42f53c25607596901bc6726d6d83c3ebf7376068ff89181d25a1bba477149480`)
.then(res => res.json())
.then(fixtures => {
LeagGamesArray = [...LeagGamesArray, fixtures]
** this.setState({
matches: LeagGamesArray
}) ** Setting the state here dosent work, just keeps running the code over and over again.**strong text**
})
}
//console.log(this.state.matches)
//console.log(this.state.matches)
//console.log(this.state.matches)
renderItem = () => {
if(this.state.leagueName != null){
return this.state.leagueName.map(function(league){
let leagueID = league.league_id
let fix = this.getMatches(leagueID)
//console.log(fix)
return (
<LeagueName key={league.country_id} league={league}/>
)
}.bind(this))//end maps..We .bind(this) b/c we want to change the meaning of this. We first use it in the .map function, but we also want to use this to call the getMacthes function.
} else {
return null;
}
}//end renderItem
render(){
return (
<ScrollView>
{this.renderItem()}
</ScrollView>
)
}
}
strong text
Your renderItem method calls getMatches(leagueID), which in turn fires-off your fetch which results in setState and a subsequent render... and repeat.
You should call getMatches from elsewhere, like componentWillMount. Or by some user interaction (or even timer) that you use to generate a refresh.

Categories

Resources