Trying to update certain fields in useState array in React Native - arrays

I'm trying to bring rows of data back from an SQLite database and then iterate through them so I can manipulate the data of each row to present them differently (ie, convert a date into a customised format).
Bringing the data back is fine (from the ReadEntries function), which I do in a useEffect so that it only runs once on the screen load, but the duplicating of the array and then updating the rows doesn't seem to work. I think it might have something to do with the fact setting the value of a useState array isn't quick enough to update for my duplicate array to then take a full snapshot of it.
It sometimes works when I save multiple times in VS Code, presumably because the state is already stored for subsequent screen refreshes.
useEffect(() => {
var results: SQLite.ResultSetRowList;
const response = async (): Promise<any> => {
await ReadEntries(projectID).then((value) => {
results = value as SQLite.ResultSetRowList;
setEntries(results.raw); //this works
let newEntries = [...entries];
for (let i = 0; i < newEntries.length; i++) {
let newStartDate = new Date(newEntries[i].startDate);
newEntries[i].dateOfEntry = newEntries[i].dateOfEntry;
newEntries[i].startDate =
newStartDate.getDate().toString() +
"/" +
newStartDate.getMonth().toString() +
"/" +
newStartDate.getFullYear().toString();
}
setEntries(newEntries);
});
};
response().catch((error) => {
"ERROR: " + error;
});
}, []);
Thanks

State updates with hooks are not only async but are also bound by closure. You should not reply on the updated state to trigger another change in the same function call
Check this post for more details on this: useState set method not reflecting change immediately
You can make use of the the fetched data to perform the operation
useEffect(() => {
var results: SQLite.ResultSetRowList;
const response = async (): Promise<any> => {
await ReadEntries(projectID).then((value) => {
results = value as SQLite.ResultSetRowList;
setEntries(results.raw); // This shouldn't be required.
let newEntries = [...results.raw]; // use results instead of entries
for (let i = 0; i < newEntries.length; i++) {
let newStartDate = new Date(newEntries[i].startDate);
newEntries[i].dateOfEntry = newEntries[i].dateOfEntry;
newEntries[i].startDate =
newStartDate.getDate().toString() +
"/" +
newStartDate.getMonth().toString() +
"/" +
newStartDate.getFullYear().toString();
}
setEntries(newEntries);
});
};
response().catch((error) => {
"ERROR: " + error;
});
}, []);

I achieved this by calling the .raw() function of the returned results, and looping through that array with forEach. I then created a new object (newarrayItem) and assigned the fields to the .raw() array values, then pushed that object into a blank array (newarray) that I assigned the type TentryItemString. Then passed this to the setEntries useState array.
useEffect(() => {
var results: SQLite.ResultSetRowList;
let newarray: TentryItemString[] = [];
const response = async (): Promise<any> => {
await ReadEntries(projectID).then((value) => {
results = value as SQLite.ResultSetRowList;
results.raw().forEach((element, index) => {
let newdate = "";
let newStartDate = new Date(element.startDate);
newdate =
newStartDate.getDate().toString() +
"/" +
(newStartDate.getMonth() + 1).toString() +
"/" +
newStartDate.getFullYear().toString();
var newarrayItem = {
dateOfEntry: "",
startDate: "",
};
newarrayItem.startDate = newdate;
newarrayItem.dateOfEntry = element.dateOfEntry;
newarray.push(newarrayItem);
});
});
newarray.reverse();
setEntries(newarray);
};
response().catch((error) => {
"ERROR: " + error;
});
}, []);
A couple of issues I found in doing this. One, if I initiated the newarrayItem by equalling it to an already created object (ie. var newarray = newarrayItem), the array.push() method seemed to overwrite all rows with the same information. That's why I've instantiated it there and then. The other issue was struggling with assigning the type to the useState as an array, but I finally achieved this with the following syntax:
const [entries, setEntries] = useState();
And all TentryItemString is, is:
type TentryItemString = {
dateOfEntry: string;
startDate: string;
};
Hope someone finds that helpful.

Related

How to resolve dependency API in useEffect beside that saga dispatch also using?

I have the address list, where the divisions, city & area data are available. As per API structure, I have to call dependent API like first division is a sole API, then as per selected division API return list of districts, then as per selected district API will return a list of areas.
On new creation, it works fine but when I am trying to update then it doesn't work properly. I am trying to use here useEffect. On the first useEffect call get division data, set the selected division option, and call city API. On the second useEffect match the selected city & call area. But unfortunately from the second useEffect it's not working properly.
useEffect Demo Code
useEffect(() => {
let selectedRegionId = '';
selectedRegionId = props?.address?.region_id;
setDivision(selectedRegionId)
let data = []
for (let i = 0; i < props.division.length; i++) {
let value = {
isSelected: props.division[i].id == selectedRegionId,
title: props.division[i].title,
id: props.division[i].id,
region_code: props.division[i].region_code
}
data.push(value)
}
let final = data.find((p) => selectedRegionId == p.id)
setItem(final)
props.selectDivision(data)
props.callGetCities(data)
}, [])
useEffect(() => {
let selectedCity = '';
let selectedCityTitle = '';
selectedCityTitle = props?.address?.city;
let cityData = []
for (let i = 0; i < props.cities.length; i++) {
let value = {
isSelected: props.cities[i].title === selectedCityTitle,
title: props.cities[i].title,
id: props.cities[i].id,
statesName: props.cities[i].statesName
}
if (selectedCityTitle.length > 0 && props.cities[i].title === selectedCityTitle) {
selectedCity = props.cities[i].id;
}
cityData.push(value)
}
setCity(selectedCity)
let cityFinal = cityData.find((p) => selectedCity == p.id)
setCItem(cityFinal)
props.selectCity(cityData)
props.callGetAreas(cityData)
setSecondNeedFetching(true);
}, [])
useEffect(() => {
let selectedArea = '';
let selectedAreaTitle = '';
selectedAreaTitle = props?.address?.postcode;
console.log('line 183', selectedAreaTitle)
let data = []
for (let i = 0; i < props.areas.length; i++) {
let value = {
isSelected: props.areas[i].title === selectedAreaTitle,
title: props.areas[i].title,
id: props.areas[i].id,
statesName: props.areas[i].statesName,
citiesName: props.areas[i].citiesName
}
if (selectedAreaTitle.length > 0 && props.areas[i].title === selectedAreaTitle) {
selectedArea = props.areas[i].id;
}
data.push(value)
}
setArea(selectedArea)
let final = data.find((p) => selectedArea == p.id)
setAItem(final)
props.selectArea(data)
}, [])
Anoter Note: On redux store only city value is stored not area cause city useEffect doesn't work
I have tried as per React useEffect hook dependent on a second effect
Also, use the dependent state by adding the following pattern
const [needFetching, setNeedFetching] = useState(false);
const [secondNeedFetching, setSecondNeedFetching] = useState(false);

Is TypeScript linter being overly cautious?

I'm getting a warning from my linter about something in this function:
React.useEffect(() => {
const uploadFiles = async (fileList: FileList) => {
const uploadSessionId = getUUID();
let mediaSetId: string | null = null;
await getMediaSetId().then(response => {mediaSetId = response});
for (let i = 0; i < fileList.length; i++) {
const file: File = fileList[i];
const originalFilename = file.name;
const fileExtension = originalFilename.split('.').pop() ?? 'unknown';
await getBase64(file).then(result => uploadFile(result, originalFilename, fileExtension, file.lastModified, mediaSetId, uploadSessionId));
}
};
if (isUploading && fileList) {
// Start the upload process
uploadFiles(fileList);
}
}, [isUploading, fileList]);
The exact error message is: Function declared in a loop contains unsafe references to variable(s) 'mediaSetId'
When building it, I first thought about putting the for loop within the .then clause but chose to do it as above, whereby I instantiate mediaSetId = null and then the async function call populates it accordingly.
Is this approach I've used "wrong"? It all seems to be working as expected by the way.

react js, why is this undefined

I have this array that is type Technology and type undefined and its making things very difficult. For example I am trying to compare this array to another array and it complains that it might be undefined etc. So I am wondering why is this undefined and how to prevent this.
This component is my main component that gets the data from the server and creates 2 different arrays holding all the data from each collection.
const App = ():JSX.Element => {
//Data from server recieve both collections in one get
const onResponse = (result:JSONData):void => {
// data received from Web API
//console.table(result);
setTechnologies(result.technologies);
setAll_Courses(result.all_courses);
setLoading(false);
};
//For error
const onError = (message:string):void => console.log("*** Error has occured during AJAX data transmission: " + message);
// ----------------------------------- setting state variables
const [technologies, setTechnologies] = React.useState<Technology[]>([]);
const [all_courses, setAll_Courses] = React.useState<AllCourses[]>([]);
// Loading
const [loading, setLoading] = React.useState<boolean>(false);
// Routing and history
const history:any = useHistory();
const route:string = useLocation().pathname;
React.useEffect(():void => {
getJSONData(COURSE_SCRIPT, onResponse, onError);
}, []);
And in my other component I make an array from the data that matches this _id in the collection of technologies. So I can then work in this component with that specific document, because I need to edit the data and display data etc. Everything is difficult because its undefined.
const EditTechnology = ({technologies, all_courses, visible}:ViewProps):JSX.Element => {
let { id } = useParams<{id:string}>();
let edit_Technology:(Technology | undefined) = technologies.find(item => item._id === id) ;
you can give props a default value in child components to prevent undefined errors, like this
const EditTechnology = ({technologies = [], all_courses = [], visible}:ViewProps):JSX.Element => {
let { id } = useParams<{id:string}>();
let edit_Technology:(Technology | undefined) = technologies.find(item => item._id === id);
}
Or put it in onResponse, in this way, you don’t need to add many default value to each child components
const onResponse = (result:JSONData):void => {
// data received from Web API
//console.table(result);
setTechnologies(result.technologies || []);
setAll_Courses(result.all_courses || []);
setLoading(false);
};
I think it maybe depends on method find
const bad = ['you','will','get', 'undefined', 'if', 'you', 'looking']
.find(e => e === 'something else')
const good = ['now', 'you', 'will','find', 'it']
.find(e => e === 'it')
console.log('for `something else` -', bad)
console.log('for `it` -', good)

wait for array to be filled using .push in react?

I am fetching an api and pushing results to an empty array, but I need the array to be filled to display the information within the array. I am getting "cannot read property high of undefined" i'm assuming because the array is not filled before rendering, how can I wait for the for loop to be complete before rendering the page?
function TopStocks(props) {
const symbols = ["AAPL", "NFLX", "GOOGL", "TSLA"];
const stockInfo = [];
useEffect(() => {
fetchSymbols();
}, []);
async function fetchSymbols() {
for (let i = 0; i < symbols.length; i++) {
await fetch(
`api&symbol=${symbols[i]}`
)
.then((res) => res.json())
.then((allStocks) => {
try {
let metaDataEntries = allStocks["Meta Data"];
let symbol = metaDataEntries["2. Symbol"].toUpperCase();
let pastDataEntries = allStocks["Time Series (Daily)"];
let pastDataValues = Object.values(pastDataEntries);
let mostRecentValue = pastDataValues[0];
let x = Object.values(mostRecentValue);
let open = parseFloat(x[0]).toFixed(2);
let high = parseFloat(x[1]).toFixed(2);
let low = parseFloat(x[2]).toFixed(2);
let close = parseFloat(x[3]).toFixed(2);
let percentage = close - open;
let result = parseFloat(percentage).toFixed(2);
stockInfo.push({
symbol: symbol,
high: high,
low: low,
close: close,
open: open,
percentage: result,
});
} catch {
console.log("surpassed the limit of 4 requests in under a minute");
}
});
}
}
return (<span className="header__grid-price">{stockInfo[0].high}</span>)
}
You are doing a simple push operation on stockInfo, which will not trigger the rerender, for that, you have to change the state of it, use useState hooks instead,
import React, { useEffect, useState } from "react";
function TopStocks(props) {
const symbols = ["AAPL", "NFLX", "GOOGL", "TSLA"];
const [stockInfo, setStockInfo] = useState([]);
useEffect(() => {
fetchSymbols();
}, []);
async function fetchSymbols() {
for (let i = 0; i < symbols.length; i++) {
await fetch(`api&symbol=${symbols[i]}`)
.then((res) => res.json())
.then((allStocks) => {
try {
let metaDataEntries = allStocks["Meta Data"];
let symbol = metaDataEntries["2. Symbol"].toUpperCase();
let pastDataEntries = allStocks["Time Series (Daily)"];
let pastDataValues = Object.values(pastDataEntries);
let mostRecentValue = pastDataValues[0];
let x = Object.values(mostRecentValue);
let open = parseFloat(x[0]).toFixed(2);
let high = parseFloat(x[1]).toFixed(2);
let low = parseFloat(x[2]).toFixed(2);
let close = parseFloat(x[3]).toFixed(2);
let percentage = close - open;
let result = parseFloat(percentage).toFixed(2);
let temp = [...stockInfo];
temp.push({
symbol: symbol,
high: high,
low: low,
close: close,
open: open,
percentage: result
});
setStockInfo(temp);
} catch {
console.log("surpassed the limit of 4 requests in under a minute");
}
});
}
}
return (
<span className="header__grid-price">
{stockInfo[0].high ? stockInfo[0].high : "loading"}
</span>
);
}
You are correct in your assumption. Try using the map function as it is async along with Promise.all so that you block the return of that function until all of your calls have finished.
...
Promise.all(
symbols.map(symbol => {
fetch(`api&symbol=${symbol}`)
.then(
// whatever you want to do with it
)
})
);
React hates it when you mutate constants. It's better to use the useState method to create the constant and the setter method.
const [stockInfo, setStockInfo] = useState([]); //This will default stockInfo to an empty array.
Then make your response dump into an array and setStockInfo() that array.
ex:
let newStockInfo = [];
/* Your stock gathering function here, but dumps into newStockInfo */
setStockInfo(newStockInfo);
That will update your stocks and prevent undefined properties.
I'm sure there is another way to do this, but this is a pretty quick way to get it done.

React.js Updating state where multiple API endpoints are involved

I'm currently trying to get a project working to test some things and I'm stuck at a point where I'm trying to update the state properly.
I have an endpoint accessed via axios.get("/docker/containers") which will return an array for all IDs of the containers which are currently running on my system this is done like so:
componentDidMount() {
this.interval = setInterval(() => this.updateContainers(), 3000);
};
componentWillUnmount() {
clearInterval(this.interval);
}
At this point my state looks like this:
state = {
containers: [{id: 'id1'}, {id: 'id2'}]
}
The user interface then just shows a list of IDs.
I can then click on an ID on my user interface and it will set a watcher:
state = {
containers: [{id: 'id1', watcher: true}, {id: 'id2'}]
}
The point of the watcher is so that on the next update cycle more detailed information about a particular container is retrieved.
state = {
containers: [{id: 'id1', watcher: true, name: 'container1'}, {id: 'id2'}]
}
Upon clicking the container in the user interface where a watcher is already set then the watcher is dropped and the more detailed information is then no longer retrieved
state = {
containers: [{id: 'id1', watcher: false}, {id: 'id2'}]
}
Where I'm getting stuck is on how to get the more detailed information. My updateContainers method has 3 steps:
Read the response from the API and destruct the state into separate variables, compare the state var with the response var and remove any containers that have gone down (no setState is done here).
Add any new containers from the response to the state that have since come up (again no setState).
...All good thus far...
Loop through the filtered array of containers from steps 1 and 2 and find any containers where a watcher is set. Where it is set perform an API call to retrieve the more detailed info. Finally set the state.
In step 3 I use a forEach on the filtered array and then do an axios.get("/docker/containers/id1") where a watcher has been set otherwise simply keep the container details I already have but that's where I get stuck, Typescript is also giving me the error:
TS2322: Type 'void' is not assignable to type 'IndividualContainer[]'.
currently I have:
updateContainers() {
axios.get('/docker/containers')
.then(response => {
const apiRequestedContainers: string[] = response.data.containers;
// array of only IDs
const stateContainers: IndividualContainer[] = [
...this.state.containers
];
// remove dead containers from state by copying still live containers
let filteredContainers: IndividualContainer[] = [
...this.filterOutContainers(stateContainers, apiRequestedContainers)
];
// add new containers
filteredContainers = this.addContainerToArray(
filteredContainers, apiRequestedContainers
);
return this.updateContainer(filteredContainers);
})
.then(finalArray => {
const newState: CState = {'containers': finalArray};
this.setState(newState);
});
};
updateContainer(containers: IndividualContainer[]) {
const returnArray: IndividualContainer[] = [];
containers.forEach(container => {
if (container.watcher) {
axios.get('/docker/containers/' + container.id)
.then(response => {
// read currently available array of containers into an array
const resp = response.data;
resp['id'] = container.id;
resp['watcher'] = true;
returnArray.push(resp);
});
} else {
returnArray.push(container);
}
return returnArray;
});
};
Any pointers to where my logic fails would be appreciated!
Edit:
Render Method:
render() {
const containers: any = [];
const curStateOfContainers: IndividualContainer[] = [...this.state.containers];
if (curStateOfContainers.length > 0) {
curStateOfContainers.map(container => {
const container_id = container.id.slice(0, 12);
containers.push(
<Container
key = {container_id}
container_id = {container.id}
name = {container.name}
clickHandler = {() => this.setWatcher(container.id)}
/>
);
});
}
return containers;
}
I'm not an expert in TypeScript so I had to change the response to JS and thought you'll re-write it in TS in case it's needed.
async updateContainers() {
const response = await axios.get('/docker/containers')
const apiRequestedContainers = response.data.containers; // array of only IDs
const stateContainers = [...this.state.containers];
// remove dead containers from state by copying still live containers
let filteredContainers = [...this.filterOutContainers(stateContainers, apiRequestedContainers)];
// add new containers
filteredContainers = this.addContainerToArray(filteredContainers, apiRequestedContainers);
const containers = await this.updateContainer(filteredContainers)
this.setState({ containers });
};
async updateContainer(containers) {
return containers.map(async (container) => {
if (container.watcher) {
const response = await axios.get('/docker/containers/' + container.id)
// read currently available array of containers into an array
return {
...response.data,
id: container.id,
watcher: true,
}
} else {
return container;
}
});
}
Here's what I've updated in updateContainer:
I'm now mapping the array instead of doing a forEach
I'm now waiting for the container details API to return a value before checking the second container. --> this was the main issue as your code doesn't wait for the API to finish ( await / async )
The problem is that you are returning nothing from updateContainer method which will return void implicitly:
// This function return void
updateContainer(containers: IndividualContainer[]) {
const returnArray: IndividualContainer[] = [];
containers.forEach(container => {
if (container.watcher) {
axios.get("/docker/containers/" + container.id).then(response => {
// read currently available array of containers into an array
const resp = response.data;
resp["id"] = container.id;
resp["watcher"] = true;
returnArray.push(resp);
});
} else {
returnArray.push(container);
}
// this is inside the forEach callback function not updateContainer function
return returnArray;
});
}
Then you assign void to containers which is supposed to be of type IndividualContainer[] so TypeScript gives you an error then you set that in the state:
updateContainers() {
axios
.get("/docker/containers")
.then(response => {
const apiRequestedContainers: string[] = response.data.containers; // array of only IDs
const stateContainers: IndividualContainer[] = [
...this.state.containers
];
// remove dead containers from state by copying still live containers
let filteredContainers: IndividualContainer[] = [
...this.filterOutContainers(stateContainers, apiRequestedContainers)
];
// add new containers
filteredContainers = this.addContainerToArray(
filteredContainers,
apiRequestedContainers
);
// this return void as well
return this.updateContainer(filteredContainers);
})
// finalArray is void
.then(finalArray => {
// you assign void to containers which should be of type IndividualContainer[]
const newState: CState = { containers: finalArray };
// containers will be set to undefined in you state
this.setState(newState);
});
}
You meant to do this:
// I added a return type here so that TypeScript would yell at me if I return void or wrong type
updateContainer(containers: IndividualContainer[]): IndividualContainer[] {
const returnArray: IndividualContainer[] = [];
containers.forEach(container => {
if (container.watcher) {
axios.get("/docker/containers/" + container.id).then(response => {
// read currently available array of containers into an array
const resp = response.data;
resp["id"] = container.id;
resp["watcher"] = true;
returnArray.push(resp);
});
} else {
returnArray.push(container);
}
// removed the return from here as it's useless
});
// you should return the array here
return returnArray;
}
First, I've commented on errors in your code:
updateContainers() {
axios.get('/docker/containers')
.then(response => {
...
return this.updateContainer(filteredContainers);
// returns `undefined`...
})
.then(finalArray => { ... });
// ...so `finalArray` is `undefined` - the reason for TS error
// Also `undefined` is not a `Promise` so this second `then()`
// doesn't make much sense
};
updateContainer(containers: IndividualContainer[]) {
const returnArray: IndividualContainer[] = [];
containers.forEach(container => {
if (container.watcher) {
axios.get('/docker/containers/' + container.id)
.then(response => {
...
returnArray.push(resp)
// because `axios.get()` is asynchronous
// this happens only some time after
// `.then(finalArray => { ... })` is finished
});
// at this moment code inside `.then()` has not been executed yet
// and `resp` has not yet been added to `returnArray`
} else {
returnArray.push(container)
// but this happens while `forEach()` is running
}
return returnArray;
// here you return from `forEach()` not from `updateContainer()`
// also `forEach()` always returns `undefined`
// so even `return containers.forEach(...)` won't work
});
// no return statement, that implicitly means `return undefined`
};
Now, why the #RocKhalil's answer, kind of, works:
async updateContainers() {
const response = await axios.get('/docker/containers')
// he favors a much clearer syntax of async/await
...
const containers = await this.updateContainer(filteredContainers)
this.setState({ containers });
};
async updateContainer(containers) {
return containers.map(async (container) => {
if (container.watcher) {
const response = await axios.get('/docker/containers/' + container.id)
// Because `axios.get()` was **awaited**,
// you can be sure that all code after this line
// executed when the request ended
// while this
// axios.get(...).then(() => console.log(2)); console.log(1)
// will lead to output 1 2, not 2 1
return {
...response.data,
id: container.id,
watcher: true,
}
} else {
return container;
}
});
// he does not forget to return the result of `map()`
// and `map()` in contrast with `forEach()` does have a result
// But...
}
But...
containers.map() returns an array. An array of Promises. Not a single Promise. And that means that
const containers = await this.updateContainer(filteredContainers)
waits for nothing. And updateContainer() function is not actually async.
To fix that you need to use Promise.all():
const containers = await Promise.all(this.updateContainer(filteredContainers))

Resources