When attempting to update an array via React state management, the state array is populated, but the user interface fails to update. The user interface only updates after I click on the navbar, and reroute to the current page (in which case useEffect does not run again, but the UI is updated).
State Code
const[isFetched, setIsFetched] = useState(false);
const[balances, setBalances] = useState<IBalance[]>([]);
const[num, setNum] = useState(0);
useEffect(() => {
console.log(balances);
// LOGS A POPULATED ARRAY
console.log(balances.length);
// LOGS 0
}, [balances]);
useEffect(() => {
const fetchBalances = async() =>{
let bals:IBalance[] = await kryptikService.getBalanceAllNetworks(kryptikWallet);
console.log("RECIEVED BALANCES:");
console.log(bals);
console.log(bals.length);
setBalances(bals);
setIsFetched(true);
}
fetchBalances();
}, []);
UI Code
<h2>Your Balances</h2>
<Divider/>
{
!isFetched?<p>Loading Balances.</p>:
<ul role="list" className="divide-y divide-gray-200 dark:divide-gray-700">
{balances.map((balance:IBalance) => (
<ListItem title={balance.fullName} imgSrc={balance.iconPath} subtitle={balance.ticker} amount={balance.amountCrypto}/>
))}
</ul>
}
</div>
Fetch Handler (called in UseEffect)
getBalanceAllNetworks = async(walletUser:IWallet):Promise<IBalance[]> =>{
let networksFromDb = this.getSupportedNetworkDbs();
// initialize return array
let balances:IBalance[] = [];
networksFromDb.forEach(async nw => {
let network:Network = new Network(nw.fullName, nw.ticker);
let kryptikProvider:KryptikProvider = await this.getKryptikProviderForNetworkDb(nw);
if(network.getNetworkfamily()==NetworkFamily.EVM){
if(!kryptikProvider.ethProvider) throw Error(`No ethereum provider set up for ${network.fullName}.`);
let ethNetworkProvider:JsonRpcProvider = kryptikProvider.ethProvider;
console.log("Processing Network:")
console.log(nw.fullName);
// gets all addresses for network
let allAddys:string[] = await walletUser.seedLoop.getAddresses(network);
// gets first address for network
let firstAddy:string = allAddys[0];
console.log(`${nw.fullName} Addy:`);
console.log(firstAddy);
console.log(`Getting balance for ${nw.fullName}...`);
// get provider for network
let networkBalance = await ethNetworkProvider.getBalance(firstAddy);
console.log(`${nw.fullName} Balance:`);
console.log(networkBalance);
// prettify ether balance
let networkBalanceAdjusted:Number = BigNumber.from(networkBalance)
.div(BigNumber.from("10000000000000000"))
.toNumber() / 100;
let networkBalanceString = networkBalanceAdjusted.toString();
let newBalanceObj:IBalance = {fullName:nw.fullName, ticker:nw.ticker, iconPath:nw.iconPath,
amountCrypto:networkBalanceString}
// add adjusted balance to balances return object
balances.push(newBalanceObj);
}
});
return balances;
}
Note: The array is a different reference, so there should be no issue with shallow equality checks. Also, the updated balances array contains objects, but the length is logged as zero as shown in the first code snippet. Any help will be much apreciated!
Issue
The issue is that you are iterating the networksFromDb array in a forEach loop with an asynchronous callback. The asynchronous callback ins't the issue, it is that Array.protptype.forEach is synchronous, the the getBalanceAllNetworks callback can't wait for the loop callbacks to resolve. It returns the empty balances array to the caller before the array is populate.
The array is still populated however, and the clicking the link is enough to trigger a React rerender and expose the mutated balances state array.
Solution
Instead of using a .forEach loop for the asynchronous callback, map networksFromDb to an array of Promises and use Promise.all and wait for them all to resolve before returning the populated balances array.
Example:
const getBalanceAllNetworks = async (
walletUser: IWallet
): Promise<IBalance[]> => {
const networksFromDb = this.getSupportedNetworkDbs();
const asyncCallbacks = networksFromDb
.filter((nw) => {
const network: Network = new Network(nw.fullName, nw.ticker);
return network.getNetworkfamily() == NetworkFamily.EVM;
})
.map(async (nw) => {
const kryptikProvider: KryptikProvider = await this.getKryptikProviderForNetworkDb(
nw
);
if (!kryptikProvider.ethProvider) {
throw Error(`No ethereum provider set up for ${network.fullName}.`);
}
const ethNetworkProvider: JsonRpcProvider = kryptikProvider.ethProvider;
// gets all addresses for network
const allAddys: string[] = await walletUser.seedLoop.getAddresses(
network
);
// gets first address for network
const firstAddy: string = allAddys[0];
// get provider for network
const networkBalance = await ethNetworkProvider.getBalance(firstAddy);
// prettify ether balance
const networkBalanceAdjusted: Number =
BigNumber.from(networkBalance)
.div(BigNumber.from("10000000000000000"))
.toNumber() / 100;
const networkBalanceString = networkBalanceAdjusted.toString();
const newBalanceObj: IBalance = {
fullName: nw.fullName,
ticker: nw.ticker,
iconPath: nw.iconPath,
amountCrypto: networkBalanceString
};
// add adjusted balance to balances return object
return newBalanceObj;
});
const balances: IBalance[] = await Promise.all(asyncCallbacks);
return balances;
};
You are mutating balances instead of updating it.
Change balances.push to setBalances(prevState => [...prevState, newBalance])
Related
Now I know the title may be a bit vague so let me help you by explaining my current situation:
I have an array worth of 100 object, which in turn contain a number between 0 and 1. I want to loop through the array and calculate the total amount e.g (1 + 1 = 2).
Currently using .map to go through every object and calaculate the total. When I am counting up using the useState hook, it kinda works. My other approach was using a Let variabele and counting up like this. Although this is way to heavy for the browser.
I want to render the number in between the counts.
const[acousticness, setAcousticness] = useState(0);
let ids = [];
ids.length == 0 && tracks.items.map((track) => {
ids.push(track.track.id);
});
getAudioFeatures(ids).then((results) => {
results.map((item) => {
setAcousticness(acousticness + item.acousticness)
})
})
return (
<div>
Mood variabele: {acousticness}
</div>
)
What is the proper way on doing this?
I think this is roughly what you are after:
import {useMemo, useEffect, useState} from 'react';
const MiscComponent = ({ tracks }) => {
// Create state variable / setter to store acousticness
const [acousticness, setAcousticness] = useState(0);
// Get list of ids from tracks, use `useMemo` so that it does not recalculate the
// set of ids on every render and instead only updates when `tracks` reference
// changes.
const ids = useMemo(() => {
// map to list of ids or empty array if `tracks` or `tracks.items` is undefined
// or null.
return tracks?.items?.map(x => x.track.id) ?? [];
}, [tracks]);
// load audio features within a `useEffect` to ensure data is only retrieved when
// the reference of `ids` is changed (and not on every render).
useEffect(() => {
// create function to use async/await instead of promise syntax (preference)
const loadData = async () => {
// get data from async function (api call, etc).
const result = await getAudioFeatures(ids);
// calculate sum of acousticness and assign to state variable.
setAcousticness(result?.reduce((a, b) => a + (b?.acousticness ?? 0), 0) ?? 0)
};
// run async function.
loadData();
}, [ids, setAcousticness])
// render view.
return (
<div>
Mood variabele: {acousticness}
</div>
)
}
I have been using Google firestore as a database for my projet.
In the collection "paths", I store all the paths I have in my app, which are composed of 2 fields : name, and coordinates (which is an array of objects with coordinates of points).
Anyway, i created a utility file in utils/firebase.js
In the file, i have this function which gets all the paths in my collection and return an array of all documents found :
export const fetchPaths = () => {
let pathsRef = db.collection('paths');
let pathsArray = []
pathsRef.get().then((response) => {
response.docs.forEach(path => {
const {nom, coordonnees } = path.data();
pathsArray.push({ nom: nom, coordonnees: coordonnees})
})
console.log(pathsArray)
return pathsArray;
});
};
In my react component, What i want to do is to load this function in useEffect to have all the data, and then display them. Here is the code I use :
import { addPath, fetchPaths } from './Utils/firebase';
//rest of the code
useEffect(() => {
let paths = fetchPaths()
setLoadedPaths(paths);
}, [loadedPaths])
//.......
The issue here is if I console log pathsArray in the function it's correct, but it never gets to the state.
When i console log paths in the component file, i get undefined.
I am quite new with react, i tried different things with await/async, etc. But I don't know what i am doing wrong here / what i misunderstand.
I know that because of my dependency, i would be supposed to have an infinite loop, but it's not even happening
Thank you for your help
Have a nice day
fetchPaths does not return any result. It should be:
export const fetchPaths = () => {
let pathsRef = db.collection('paths');
let pathsArray = []
return pathsRef.get().then((response) => {
response.docs.forEach(path => {
const {nom, coordonnees } = path.data();
pathsArray.push({ nom: nom, coordonnees: coordonnees})
})
console.log(pathsArray)
return pathsArray;
});
};
note the return statement.
Since the fetchPaths returns a promise, in the effect it should be like following:
useEffect(() => {
fetchPaths().then(paths =>
setLoadedPaths(paths));
}, [loadedPaths])
Does anyone have a way to delete id before passing through url?
I want to remove the id that is in the addedItems array.
Delete only id. Without interfering with the value inside
onFinish = async(values) => {
const { addedItems, total } = this.state;
values.re_total = total;
const { data, result } = this.props.loginReducer;
await addedItems.forEach((doc) => {
doc.car_number = values.cus_car_number;
doc.reference = values.re_reference;
doc.detail = values.re_detail;
doc.adName = result.data.u_fname;
doc.total = doc.price * doc.quantity;
});
delete addedItems.id;
console.log(addedItems)
await httpClient
.post(`http://localhost:8085/api/v1/revenue/revenue`,{addedItems})
.then((res) => {
console.log(res);
})
.catch((error) => {
console.log("Error :", error);
})
message.success({ content: 'บันทึกรายรับเรียบร้อย!', duration: 2, style: {
marginTop: '5vh',
}} ,100);
await this.props.history.goBack();
};
I'm assuming that the id is in each item of the addedItems array. Rather than it being added to the array directly (addedItems.id = 'bad').
I switched from using a Array#forEach to Array#map so that it creates a new array. Then I use object destructuring with the rest operator (...). So I can pull the id out of the doc object and make a shallow copy of the doc at the same time. The shallow copy is important so we don't accidentally modify state values without meaning to.
That's actually the biggest issue in your code as is - it's modifying the this.state.addedItems unintentionally, as you are changing the docs without doing a shallow copy first.
As for the code: Replace the await addedItems.forEach() with this. You didn't actually need an await there, as Array#forEach doesn't return anything let alone a promise.
const mappedItems = addedItems.map(({id,...doc})=>{
doc.car_number = values.cus_car_number;
doc.reference = values.re_reference;
doc.detail = values.re_detail;
doc.adName = result.data.u_fname;
return doc;
})
Then use {addedItems: mappedItems} as the body of the httpClient.post. So that you use the new array instead of the old one pulled from the state.
I am writing an app for vehicle tracking. With the help of Google Maps API, I am able to get directions and extract all the required info. The problem appeared with Elevations API responses. From DirectionRender class I am sending path and distance as props. GM Elevations request is done via elevator.getElevationAlongPath(option,PlotElevation). PlotElevation (elevations,status) is a callback function. However, no matter how I try to receive just one response from it (using useMemo, useEffect, I think I tried everything), still, there are problems with the re-rendering of responses. OVER_QUERY_LIMIT or endless re-render. Could someone help with that?
Thanks
const Elevation = React.memo(props =>{
const [path, setPath] = useState({...props.path})
const [distance, setDistance]=useState({...props.distance})
const [elevationArray, setElevationArray] = useState(null)
const [stop, setStop] = useState(false)
let pathElev = JSON.parse(JSON.stringify(path))
React.useEffect(() => {
setPath(props.path)
}, [props.path])
React.useEffect(() => {
setDistance(props.distance)
}, [props.distance])
let elevator = new window.google.maps.ElevationService;
let numberSamples = parseInt( distance/40)
let options = {
'path':path,
'samples':numberSamples
}
//The problem starts here
const PlotElevation = (elevations, status) => {
if (stop === false){
console.log('status',status)
console.log(JSON.parse(JSON.stringify(elevations)))
setElevationArray(elevations)
//setStop(true)
console.log(elevations[19].elevation)
return
}
}
const Memo = React.useMemo(
()=>{
elevator.getElevationAlongPath(
options,PlotElevation
)
},[elevator.getElevationAlongPath(
options,PlotElevation
)])
// elevator.getElevationAlongPath(
// {
// path: path,
// samples: 100
// }, elevations =>{
// setElevationArray({
// // We’ll probably want to massage the data shape later:
// // elevationArray: elevations
// })
// }
// )
return (
<div>
{Memo}
{console.log('path is received ', pathElev)}
{console.log('number of samples', numberSamples)}
{console.log('elevation check ',elevationArray)}
{/* {elevator.getElevationAlongPath(
options,PlotElevation)} */}
</div>
)
})
export default Elevation
https://github.com/reduxjs/reselect/blob/master/src/index.js#L89
export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {
let lastArgs = null
let lastResult = null
// we reference arguments instead of spreading them for performance reasons
return function () {
if (!areArgumentsShallowlyEqual(equalityCheck, lastArgs, arguments)) {
// apply arguments instead of spreading for performance.
lastResult = func.apply(null, arguments)
}
lastArgs = arguments
return lastResult
}
}
export function createSelectorCreator(memoize, ...memoizeOptions) {
return (...funcs) => {
let recomputations = 0
const resultFunc = funcs.pop()
const dependencies = getDependencies(funcs)
const memoizedResultFunc = memoize(
function () {
recomputations++
// apply arguments instead of spreading for performance.
return resultFunc.apply(null, arguments)
},
...memoizeOptions
)
...}
}
export const createSelector = createSelectorCreator(defaultMemoize)
So if I create createSelector(getUsers, (users) => users) for making a simple example. How is it run behind with the codes from above ?
createSelectorCreator(defaultMemoize) is called with getUsers, (users) => users inputs. Now defaultMemoize is also a function that returns a function. How are they all interacting to return the value ?
I think more important to how reselect works is why one should use it. The main reasons are composability and memomization:
Composability
Another way of saying this is that you write a selector once and re use it in other more detailed selectors. Lets say I have a state like this: {data:{people:[person,person ...]} Then I can write a filterPerson like this:
const selectData = state => state.data;
const selectDataEntity = createSelector(
selectData,//re use selectData
(_, entity) => entity,
(data, entity) => data[entity]
);
const filterDataEntity = createSelector(
selectDataEntity,//re use selectDataEntity
(a, b, filter) => filter,
(entities, filter) => entities.filter(filter)
);
If I move data to state.apiResult then I only need to change selectData, this maximizes re use of code and minimizes duplication of implementation.
Memoization
Memoization means that when you call a function with the same arguments multiple times the function will only be executed once. Pure functions return the same result given the same arguments no matter how many times they are called or when they are called.
This means that you don't need to execute the function when you call it again with the same parameters because you already know the result.
Memoization can be used to not call expensive functions (like filtering a large array). In React memoization is important because pure components will re render if props change:
const mapStateToProps = state => {
//already assuming where data is and people is not
// a constant
return {
USPeople: state.data.people.filter(person=>person.address.countey === US)
}
}
Even if state.data.people didn't change the filter function would return a new array every time.
How it works
Below is a re write of createSelector with some comments. Removed some code that would safety check parameters and allow you to call createSelector with an array of functions. Please comment if there is anything you have difficulty understanding.
const memoize = fn => {
let lastResult,
//initial last arguments is not going to be the same
// as anything you will pass to the function the first time
lastArguments = [{}];
return (...currentArgs) => {//returning memoized function
//check if currently passed arguments are the same as
// arguments passed last time
const sameArgs =
currentArgs.length === lastArguments.length &&
lastArguments.reduce(
(result, lastArg, index) =>
result && Object.is(lastArg, currentArgs[index]),
true
);
if (sameArgs) {
//current arguments are same as last so just
// return the last result and don't execute function
return lastResult;
}
//current arguments are not the same as last time
// or function called for the first time, execute the
// function and set last result
lastResult = fn.apply(null, currentArgs);
//set last args to current args
lastArguments = currentArgs;
//return result
return lastResult;
};
};
const createSelector = (...functions) => {
//get the last function by popping it off of functions
// this mutates functions so functions does not have the
// last function on it anymore
// also memoize the last function
const lastFunction = memoize(functions.pop());
//return a selector function
return (...args) => {
//execute all the functions (last was already removed)
const argsForLastFunction = functions.map(fn =>
fn.apply(null, args)
);
//return the result of a call to lastFunction with the
// result of the other functions as arguments
return lastFunction.apply(null, argsForLastFunction);
};
};
//selector to get data from state
const selectData = state => state.data;
//select a particular entity from state data
// has 2 arguments: state and entity where entity
// is a string (like 'people')
const selectDataEntity = createSelector(
selectData,
(_, entity) => entity,
(data, entity) => data[entity]
);
//select an entity from state data and filter it
// has 3 arguments: state, entity and filterFunction
// entity is string (like 'people') filter is a function like:
// person=>person.address.country === US
const filterDataEntity = createSelector(
selectDataEntity,
(a, b, filter) => filter,
(entities, filter) => entities.filter(filter)
);
//some constants
const US = 'US';
const PEOPLE = 'people';
//the state (like state from redux in connect or useSelector)
const state = {
data: {
people: [
{ address: { country: 'US' } },
{ address: { country: 'CA' } },
],
},
};
//the filter function to get people from the US
const filterPeopleUS = person =>
person.address.country === US;
//get people from the US first time
const peopleInUS1 = filterDataEntity(
state,
PEOPLE,
filterPeopleUS
);
//get people from the US second time
const peopleInUS2 = filterDataEntity(
state,
PEOPLE,
filterPeopleUS
);
console.log('people in the US:', peopleInUS1);
console.log(
'first and second time is same:',
peopleInUS1 === peopleInUS2
);