Lifecycle of useState hook in React.js - reactjs

I have the following synchronism problem. Given that I know that the React useState hook is asynchronous, I run into the following: I'm downloading some images from Amazon S3, I manage to save it correctly in my hook: defaultSelfiePicture and depending on the weight of the image (or so I think) sometimes I get the images loaded correctly and sometimes not. I have tried to force state changes after I finish saving the object in my hook but it never renders the image, only if I change component and come back is when it is shown in the cases that it takes longer to load.
const [defaultSelfiePictures, setDefaultSelfiePictures] = useState([])
useEffect(() => {
if (savedUser.docs !== undefined) {
loadAllPictures()
}
}, [savedUser.docs.length])
const loadAllPictures = () => {
let p1 = loadUrlDefaultFrontPictures()
let p2 = loadUrlDefaultBackPictures()
let p3 = loadUrlDefaultSelfiePictures()
Promise.all([p1, p2, p3]).then(result => {
console.log('end all promises')
setTimestamp(Date.now())
})
}
const loadUrlDefaultSelfiePictures = async () => {
if (savedUser.docs.length > 0) {
let readedPictures = []
for (let i = 0; i < savedUser.docs.length; i++) {
if (
savedUser.docs[i].type === 'SELFIE'
//&& savedUser.docs[i].side === 'FRONT'
) {
if (
savedUser.docs[i].s3Href !== null &&
savedUser.docs[i].s3Href !== undefined
) {
const paramsKeyArray =
savedUser.docs[i].s3Href.split('')
let paramsKey = paramsKeyArray.pop()
let params = {
Bucket: process.env.REACT_APP_S3_BUCKET,
Key: paramsKey
}
await s3.getSignedUrl('getObject', params, function (err, url) {
readedPictures.push({
idKycDoc: savedUser.docs[i].idKycDoc,
name: 'selfie.jpeg',
type: savedUser.docs[i].type,
url: url
})
})
} else {
let urlPicture = savedUser.docs[i].localHref
let response = await axios.get(`${URL_IMG}${urlPicture}`, {
responseType: 'blob'
})
function readAsDataURL(data) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(data)
reader.onloadend = () => {
resolve(reader.result)
}
})
}
const base64Data = await readAsDataURL(response.data)
readedPictures.push({
idKycDoc: savedUser.docs[i].idKycDoc,
name: 'selfie.jpeg',
type: savedUser.docs[i].type,
url: `data:image/jpeg;base64,${base64Data.slice(21)}`
})
}
}
}
setDefaultSelfiePictures(readedPictures)
}
}
And I obtain this :
I can see that the hook has content, but that content is not updated until the next rendering of the component, also if I try to make any changes when I detect that the .length has changed it tells me that it is 0...
And right after the next render I get this:

Related

getDownloadURL in array of dictionary (re-rendering issue, forEach)

I have an array of dictionaries, (e.g. [{}, {}, {}, {}], each dictionary contains information about book)
I want to download image from firebase storage using getDownloadURL.
My current code's like...
const [resObj, setresObj] = useState() // empty variable for update state
let result = [] //create empty array for copy & push new obj
useEffect(() => {
props.resObj.forEach((obj) => { // props.resObj: array of dictionary I explained before
const jpgName = 'bookDB/'+ obj.도서번호 + '.jpg';
const imgRef = ref(storage, jpgName)
getDownloadURL(imgRef)
.then((url) => {
result1.push({
...obj,
bookUrl: url
}) // copy & push dictionary
})
.catch((error) => {
if (error.code === 'storage/object-not-found') {
console.log('이미지 파일 없음')
result1.push({
...obj,
bookUrl: "https://upload.wikimedia.org/wikipedia/commons/a/ac/No_image_available.svg"
})
} else { console.log(error)}
})
})
setresObj(result1)
}, [])
after this code update 'resObj' variable,
I map resObj in component like...
return (
<div>
{resObj? resObj.map(item => {
<img
key = {}
className = '~~'
onClick = {}
src = {item.bookUrl}
/>
})}
</div>
)
unfortunately.. it doesn't show nothing..
It seems that forEach, useEffect, useState, getDownloadURL Promise seriously entangled..
I tried 1) devide download image code as function, 2) devide download image code as recoil, 3) escape download image code from useEffect, 4) ...(extra variances of code)...
The problem is that your call to setresObj happens before any of the calls to result1.push have happened, so you're always setting an empty array. It's easiest to verify this by setting breakpoints and running in the debugger, or by adding some console.log calls.
The fix is to use Promise.all to wait for all download URLs to have been retrieved and only then call setresObj. Something like this:
useEffect(() => {
let promises = props.resObj.map((obj) => {
const jpgName = 'bookDB/'+ obj.도서번호 + '.jpg';
const imgRef = ref(storage, jpgName)
return getDownloadURL(imgRef)
.then((url) => {
return {
...obj,
bookUrl: url
}
})
.catch((error) => {
if (error.code === 'storage/object-not-found') {
console.log('이미지 파일 없음')
result1.push({
...obj,
bookUrl: "https://upload.wikimedia.org/wikipedia/commons/a/ac/No_image_available.svg"
})
} else { console.log(error)}
})
})
Promise.all(promises).then((results) => {
setresObj(results);
});
}, [])

How can I await for state to be set if it doesn't return a promise?

I am trying to set state to my setStockInfo hook and wait for the for it to be set before I run props.onInitialSet(). I tried passing setStockInfo as a dependency in the useEffect, but getting no luck. I need the information from stockInfo before I set to the global store. Is there a way I can use a callback since useState doesn't return a promise? The error returning is "cannot read property symbol of undefined" from the props.initialSet()
function TopStocks(props) {
const [stockInfo, setStockInfo] = useState([]);
const symbols = ["AAPL", "NFLX", "GOOGL", "TSLA"];
let temp = [];
useEffect(() => {
fetchSymbols();
props.onInitialSet(
stockInfo[2].symbol,
stockInfo[2].percentage,
stockInfo[2].close
);
}, [setStockInfo]);
async function fetchSymbols() {
for (let i = 0; i < symbols.length; i++) {
await fetch(
`api${symbols[i]}`
)
.then((res) => res.json())
.then((allStocks) => {
console.log(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 colorToSend;
let percentage = close - open;
if (percentage < 0) {
colorToSend = "red";
} else {
colorToSend = "rgb(30, 216, 139)";
}
let result = parseFloat(percentage).toFixed(2);
temp.push({
symbol: symbol,
high: high,
low: low,
close: close,
open: open,
percentage: result,
color: colorToSend,
});
} catch {
console.log("surpassed the limit of 4 requests in under a minute");
}
});
}
setStockInfo(temp);
}
}
const mapDispatchToProps = (dispatch) => {
return {
onInitialSet: (symbol, percentage, price) => {
dispatch({
type: "INITIALSET",
value: {
price: price,
symbol: symbol,
percentage: percentage,
},
});
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TopStocks);
stockInfo is the value that will change, not the state setter. Inside the useEffect, check to see if the array has been populated:
// Run once, on mount:
useEffect(fetchSymbols, []);
// Run after fetchSymbols finishes:
useEffect(() => {
if (stockInfo.length) {
props.onInitialSet(
stockInfo[2].symbol,
stockInfo[2].percentage,
stockInfo[2].close
);
}
}, [stockInfo]);
You should also put a try/catch around the await fetch so you can catch possible network errors.
Given that useEffect runs after every render, the fetched stockInfo is not reflected until after the current rendering process.
Thus, stockInfo is undefined when passed to onInitialSet.
To solve this, follow the Single Responsibility Principle by simply splitting to two effects:
First effect is to fetch symbols
Set initial set
const [stockInfo, setStockInfo] = useState([]);
// First effect: to fetch
useEffect(async () => {
let temp;
for (let i = 0; i < symbols.length; i++) {
await fetch(`api${symbols[i]}`);
// TODO: add rest of code
}
setStockInfo(temp);
}, []);
// 2nd effect: call onInitialSet
useEffect(() => {
if (stockInfo.length === 0) {
return;
}
props.onInitialSet(
stockInfo[2].symbol,
stockInfo[2].percentage,
stockInfo[2].close
);
}, [stockInfo]);

(Refactor/Improve) Loop to make API calls and manupilate Array following the "no-loop-func"

Despite looking and following numerous answers here at stackoverflow,I have still failed to refactor this code to abide by the ESLint no-loop-func.
I keep getting the following warning, despite my efforts to refactor the code:
Compiled with warnings.
Function declared in a loop contains unsafe references to variable(s) 'lastResult', 'biologyBooks', 'page' no-loop-func
Here's the code:
import React from 'react';
import { apiFullCall } from '../../apiHelper';
const MyComponent = props => {
const [state, setState] = React.useState({ total: 0, biologyBooksByAuthor: [] });
let isLoaded = React.useRef(true);
const token = sessionStorage.getItem('token');
const authorID = sessionStorage.getItem('author_id');
const getBooks = async() => { // fetch items
let page = 1;
let scienceBooks, biologyBooks;
// create empty arrays to store book objects for each loop
let scienceBooks = biologyBooks = [];
// create a lastResult object to help check if there is a next page
let lastResult = { next: null };
do { // the looping - this is what I have failed to refactor
try {
await apiFullCall( // Make API calls over paginated records
'',
token,
'get',
`books/?author_id=1&page=${page}`
).then(res => {
if (res) {
const { status, body } = res;
if (status === 200 || status === 201) {
lastResult = body; // assign lastResult to pick "next"
body &&
body.results &&
body.results.map(eachBook => { // we map() over the returned "results" array
// the author with queried "author_id" writes science books;
// so we add each book (an object) into the science category
scienceBooks.push(eachBook);
// We then filter the author's biology books (from other science books)
biologyBooks = scienceBooks.filter(
({ is_biology }) =>
typeof(is_biology) === "boolean" && is_biology === true
);
return null;
}
);
// increment the page with 1 on each loop
page++;
}
}
}).catch(error => console.error('Error while fetching data:', error));
} catch (err) { console.error(`Oops, something went wrong ${err}`); }
// keep running until there's no next page
} while (lastResult.next !== null);
// update the state
setState(prevState => ({
...prevState, total: scienceBooks.length, biologyBooksByAuthor: biologyBooks,
}));
};
React.useEffect(() => { // fetch science books by author (logged in)
if (isLoaded && authorID) {
getBooks();
};
return function cleanup() {...}; // clean up API call, on unmount
}, [isLoaded, authorID]);
return (
// render the JSX code
);
}
Please note that I actually declared the said variables lastResult, biologyBooks and page outside the "do-while".
Any help or clues will be greatly appreciated.
The function the warning is referring to is the .then callback, if you're using async/await stick to it, try removing the .then part by assigning the result to a variable instead and remove the unnecessary .map, you can concatenate previous results with spread operator or .concat.
import React from 'react';
import { apiFullCall } from '../../apiHelper';
const MyComponent = props => {
const [state, setState] = React.useState({
total: 0,
scienceBooksByAuthor: [],
});
const isLoaded = React.useRef(true);
const token = sessionStorage.getItem('token');
const authorID = sessionStorage.getItem('author_id');
const getBooks = async () => {
// fetch items
let page = 1;
let scienceBooks = [];
// create a lastResult object to help check if there is a next page
let lastResult = { next: null };
do {
// the looping - this is what I have failed to refactor
try {
const res = await apiFullCall(
// Make API calls over paginated records
'',
token,
'get',
`books/?author_id=1&page=${page}`,
);
if (res) {
const { status, body } = res;
if (status === 200 || status === 201) {
lastResult = body; // assign lastResult to pick "next"
// concatenate new results
scienceBooks = [
...scienceBooks,
...((lastResult && lastResult.results) || []),
];
// increment the page with 1 on each loop
page += 1;
}
}
} catch (err) {
console.error(`Oops, something went wrong ${err}`);
}
// keep running until there's no next page
} while (lastResult.next !== null);
const biologyBooks = scienceBooks.filter(
({ is_biology }) =>
typeof is_biology === 'boolean' && is_biology === true,
);
// update the state
setState(prevState => ({
...prevState,
total: scienceBooks.length,
scienceBooksByAuthor: scienceBooks,
}));
};
React.useEffect(() => {
// fetch science books by author (logged in)
if (isLoaded && authorID) {
getBooks();
}
return function cleanup() {...}; // clean up API call, on unmount
}, [isLoaded, authorID]);
return (
// render the JSX code
);
};

Why when the state is updated the changes in the rendering are not updated?

I have created a hook to access the collections of the database and its methods.
import { remote } from 'electron'
import { useState, useEffect } from "react"
function useCollections(collections = []) {
let [dbInstances, setDbInstances] = useState(null)
let [data, setData] = useState(null)
// DB METHODS
// Create
let create = async (doc, dbName) => {
await dbInstances[dbName].create(doc)
let newData = await dbInstances[dbName].readAll()
setData({ ...data, [dbName]: newData })
}
// Get details
let getDetails = async (id, dbName) => {
let doc = await dbInstances[dbName].read(id)
return doc
}
// Delete
let deleteOne = async (id, dbName) => {
await dbInstances[dbName].deleteOne(id)
let newData = await dbInstances[dbName].readAll()
setData({ ...data, [dbName]: newData })
}
// Update
let updateOne = async (id, updatedDoc, dbName) => {
await dbInstances[dbName].archive(id, updatedDoc)
let newData = await dbInstances[dbName].readAll()
setData({ ...data, [dbName]: newData })
}
// EFFECTS
useEffect(() => {
console.log('mounting component')
let newDBIs = {}
collections.forEach(col => newDBIs[col] = remote.getGlobal(col))
console.log('db instances settted', newDBIs)
setDbInstances(newDBIs)
}, [])
// When DBs are instantiated, request all docs and set data with response
useEffect(() => {
if (
dbInstances !== null &&
data === null &&
Object.keys(dbInstances).length === collections.length)
{
console.log('setting data')
let newData = {}
collections.forEach(async col => newData[col] = await dbInstances[col].readAll())
console.log('data setted => ', newData)
setData(newData)
}
}, [dbInstances])
return {
data,
create,
getDetails,
deleteOne,
updateOne
};
}
export default useCollections;
In the component where the data returned by the hook is consumed, even though the variable data contains the expected data, these are not rendered.
import WindowsLayout from "../../components/layout/WindowsLayout"
import { useState, useEffect } from "react"
import { remote } from "electron"
import useCollections from "../../hooks/useCollections"
const EditWorkWindow = ({ workId }) => {
let { data, deleteOne, updateOne } = useCollections([
'workDB',
'studioDB',
'rateDB'
])
useEffect(() => {
if (data !== null) console.log(data)
}, [data])
return (
<WindowsLayout title="Edit work window">
<div style={{ height: 243 }} className="window-content">
<div className="padded-more bg-gray-200">
<h2>{JSON.stringify(data)}</h2>
<button onClick={() => console.log(data)}>CLG</button>
</div>
</div>
</WindowsLayout >
)
}
export default EditWorkWindow
The effect hook shows the expected data by console.
<h2>{JSON.stringify(data)}</h2> = {}
When the button is clicked, the expected data is displayed on the console.
I can't understand why if data contains properties, they are not shown in {JSON.stringify(data)}
This is what is shown by the console after clicking on the button
console.log(data) image
And this is example data and their its properties
{
"workDB":[
{
"product":"Work name 1",
"amounts_rates":[
{
"rate":"EflcQflqu2oWWVk2",
"amount":6
},
{
"rate":"FeMIX00pwpmZwoVW",
"amount":1
}
],
"date":"2020-08-31",
"studio":"BCvPeWzMiS8fZsmS",
"_id":"2ZvHMWFODBHYWEBo",
"createdAt":"2020-08-31T09:39:21.077Z",
"updatedAt":"2020-08-31T09:39:21.077Z"
},
{
"product":"Work name 2",
"amounts_rates":[
],
"date":"2020-09-02",
"director":"",
"_id":"PRpp1OQcJnkFKeR0",
"createdAt":"2020-09-01T19:56:33.201Z",
"updatedAt":"2020-09-01T19:56:33.201Z"
}
],
"studioDB":[
{
"name":"Studio name 1",
"_id":"0J1AVXtgDjwBjRS9",
"createdAt":"2020-08-25T10:18:40.004Z",
"updatedAt":"2020-08-25T10:18:40.004Z"
},
{
"name":"Studio name 2",
"_id":"8sFH7gncaM6V7lHh",
"createdAt":"2020-08-25T10:19:45.232Z",
"updatedAt":"2020-08-25T10:19:45.232Z"
}
],
"rateDB":[
{
"name":"Rate name 1",
"value":4.1,
"_id":"EflcQflqu2oWWVk2",
"createdAt":"2020-08-25T10:24:17.357Z",
"updatedAt":"2020-08-25T10:24:17.357Z"
},
{
"name":"Rate name 1",
"value":34,
"_id":"FeMIX00pwpmZwoVW",
"createdAt":"2020-08-25T10:24:25.628Z",
"updatedAt":"2020-08-25T10:24:25.628Z"
}
]
}
Async problem it is.
// When DBs are instantiated, request all docs and set data with response
useEffect(() => {
if (
dbInstances !== null &&
data === null &&
Object.keys(dbInstances).length === collections.length)
{
console.log('setting data')
let newData = {}
collections.forEach(async col => newData[col] = await dbInstances[col].readAll())
console.log('data setted => ', newData)
setData(newData) // <-- 👋LOOK HERE
}
}, [dbInstances])
So you let newData = {} empty object, and send it off to trigger an update re-render by calling setData(), but newData is empty at the moment of calling.
In your rendering function JSON.stringify(data) pickup the data, but it's still empty at the moment of rendering!
It's only when async col => newData[col] = await someValue call is resolved, that your newData's properties will get assigned new values, the newData object stays the same. But by the time it's resolved, the rendering is done already.
Solution: wait till async function call is resolved, then you call setData()
useEffect(() => {
// ...
const promises = collections.map(async col => {
newData[col] = await dbInstances[col].readAll())
})
Promise.all(promises).then(() => { setData(newData) })
})
The reason why you see the updated value when inspecting in console, is because you didn't inspect "quick enough". By the time you mouse-click to expand the object in console, its properties are already assigned values. But if you change
console.log('data setted => ', newData)
// to
console.log('data setted => ', JSON.stringify(newData))
you'll see an empty object.

React State is not updated with socket.io

When page loaded first time, I need to get all information, that is why I am calling a fetch request and set results into State [singleCall function doing that work]
Along with that, I am connecting websocket using socket.io and listening to two events (odds_insert_one_two, odds_update_one_two), When I got notify event, I have to
check with previous state and modify some content and update the state without calling again fetch request. But that previous state is still [] (Initial).
How to get that updated state?
Snipptes
const [leagues, setLeagues] = useState([]);
const singleCall = (page = 1, params=null) => {
let path = `${apiPath.getLeaguesMatches}`;
Helper.getData({path, page, params, session}).then(response => {
if(response) {
setLeagues(response.results);
} else {
toast("Something went wrong, please try again");
}
}).catch(err => {
console.log(err);
})
};
const updateData = (record) => {
for(const data of record) {
var {matchId, pivotType, rateOver, rateUnder, rateEqual} = data;
const old_leagues = [...leagues]; // [] becuase of initial state value, that is not updated
const obj_index = old_leagues.findIndex(x => x.match_id == matchId);
if(obj_index > -1) {
old_leagues[obj_index] = { ...old_leagues[obj_index], pivotType, rateOver: rateOver, rateUnder:rateUnder, rateEqual:rateEqual};
setLeagues(old_leagues);
}
}
}
useEffect(() => {
singleCall();
var socket = io.connect('http://localhost:3001', {transports: ['websocket']});
socket.on('connect', () => {
console.log('socket connected:', socket.connected);
});
socket.on('odds_insert_one_two', function (record) {
updateData(record);
});
socket.on('odds_update_one_two', function (record) {
updateData(record);
});
socket.emit('get_odds_one_two');
socket.on('disconnect', function () {
console.log('socket disconnected, reconnecting...');
socket.emit('get_odds_one_two');
});
return () => {
console.log('websocket unmounting!!!!!');
socket.off();
socket.disconnect();
};
}, []);
The useEffect hook is created with an empty dependency array so that it only gets called once, at the initialization stage. Therefore, if league state is updated, its value will never be visible in the updateData() func.
What you can do is assign the league value to a ref, and create a new hook, which will be updated each time.
const leaguesRef = React.useRef(leagues);
React.useEffect(() => {
leaguesRef.current = leagues;
});
Update leagues to leaguesRef.current instead.
const updateData = (record) => {
for(const data of record) {
var {matchId, pivotType, rateOver, rateUnder, rateEqual} = data;
const old_leagues = [...leaguesRef.current]; // [] becuase of initial state value, that is not updated
const obj_index = old_leagues.findIndex(x => x.match_id == matchId);
if(obj_index > -1) {
old_leagues[obj_index] = { ...old_leagues[obj_index], pivotType, rateOver:
rateOver, rateUnder:rateUnder, rateEqual:rateEqual};
setLeagues(old_leagues);
}
}
}

Resources