React does not rerender on updated state of nested array - reactjs

I have an array of objects like so:
const [categories, setCategories] = React.useState([
{
id: 1,
title: 'Top Picks',
subTitle: "Today's hottest stuff",
images: [],
searchQuery: 'shoes',
},
...]);
Which I update with values in useEffect once like so:
React.useEffect(() => {
const newCategories = categories.map(category => {
fetch(`https://api.pexels.com/v1/search?query=${category.searchQuery}&per_page=10`, {
headers: {
'Authorization': apiKey,
},
}).then(r => {
r.json().then(convertedJson => {
category.images = convertedJson.photos;
});
});
return category;
});
setCategories(newCategories);
}, []);
however the child components here never rerender and I cannot figure out why. My understanding is that .map creates a new array anyhow, so the spread syntax isn't necessary in setCategories() but regardless it does not work.
{categories.map((category, i) => (
<CategorySlider {...category} key={i}/>
))}

There's a few issues but the primary issue I see is you're returning the category before the fetch can complete - so even when those fetch calls inside your map complete, you already returned the category below before the fetch completes.
Try using the .finally() block:
React.useEffect(() => {
const newCategories = categories.map(category => {
const c = {...category}; // <--- good practice
fetch(`https://api.pexels.com/v1/search?query=${category.searchQuery}&per_page=10`, {
headers: {
'Authorization': apiKey,
},
}).then(r => {
r.json().then(convertedJson => {
category.images = convertedJson.photos;
});
}).catch((err) => {
console.error(err);
}).finally(() => {
return category;
});
});
setCategories(newCategories);
}, []);

Thanks! Using setState before the promises resolved was indeed the problem. The solution looks like this now:
React.useEffect(() => {
async function fetchImages() {
const promises = categories.map(async category => {
const response = await fetch(
`https://api.pexels.com/v1/search?query=${category.searchQuery}&per_page=10`,
{
headers: {
Authorization: apiKey,
},
}
);
const convertedJson = await response.json();
category.images = convertedJson.photos;
return category;
});
setCategories(await Promise.all(promises));
}
fetchImages();
}, []);

Related

building dinamically array for react-simple-image-slider

React simple slider needs array of images at the star and some code inside html to initiate the slider
const images = [
{ url: "images/1.jpg" },
{ url: "images/2.jpg" },
{ url: "images/3.jpg" },
{ url: "images/4.jpg" },
{ url: "images/5.jpg" },
{ url: "images/6.jpg" },
{ url: "images/7.jpg" },
];
for images array I made
const [images, setImages] = useState([]);
maybe different way is better correct me pls.
Then I have useEffect where I fetch the data
useEffect(() => {
const getData= async () => {
try {
const response = await fetch(`url`, {
fetch request....
const ImagesArray = imagesArrayfromFetch.map((image) => ({
url: `/img/${image}`,
}));
console.log(ImagesArray);
setImages(ImagesArray);
console.log(images);
When I console.log ImagesArray - it gives me correctly filled array object.
console.log(images) gives me undefined. Here is an error probably
Then inside html I build the slider object
const App = () => {
return (
<div>
<SimpleImageSlider
width={896}
height={504}
images={images}
showBullets={true}
showNavs={true}
/>
</div>
);
}
So because setImages do not put array inside images slider object creates without images.
Is there a way to fix this?
It seems like you have a race condition.
Setting new state must happen after resolve.
Could be done in fetch().then( /* set state */ )
Cleaner way would be with await/async:
const fetchImages = async () => {
try {
const data = await fetch(...);
if (data) {
const ImagesArray = data.images.map((image) => ({
url: `/img/${image}`,
}));
console.log(ImagesArray);
setImages(ImagesArray);
}
} catch(error) {
console.error(error);
}
}
useEffect(() => fetchImages(), [])

How can i test downloading excel file with jest?

I am new to testing react components with jest and enzyme. I have this example
generateExcelFile = () => {
const {actions, state} = this.props;
const dateFrom = state.getIn(['config', 'marketingQuestionReport', 'dateFrom']);
const dateTo = state.getIn(['config', 'marketingQuestionReport', 'dateTo']);
this.setState({isLoading: true, isLoadingFinished: false});
fetch(`${env.MARKETING_QUESTION_REPORT}?dateFrom=${dateFrom}&dateTo=${dateTo}`)
.then((resp) => resp.blob())
.then((blob) => {
if (typeof window.navigator.msSaveBlob !== 'undefined') {
window.navigator.msSaveBlob(
blob,
`Marketing Question Report from ${dateFrom} to ${dateTo}.xlsx`
);
}
const url = window.URL.createObjectURL(blob);
const tempLink = document.createElement('a');
tempLink.style.display = 'none';
tempLink.href = url;
tempLink.setAttribute(
'download',
`Marketing Question Report from ${dateFrom} to ${dateTo}.xlsx`
);
if (typeof tempLink.download === 'undefined') {
tempLink.setAttribute('target', '_blank');
}
document.body.appendChild(tempLink);
tempLink.click();
window.URL.revokeObjectURL(url);
toastr.success('Sucessfully generated marketing question report');
this.setState({isLoading: false, isLoadingFinished: true});
actions.clearMarketingQuestionReportDates();
})
.catch(() =>
toastr.error('An error occurred while generating marketing question report')
);
};
I am struck on .then part i don't know how to test after that the whole fetch call
What i have sofar
describe('<MarketingQuestionReportPage />', () => {
beforeEach(() => {
const fecthSpy = jest.spyOn(window, 'fetch').mockReturnValue(() =>
Promise.resolve({
blob: () =>
Promise.resolve({
size: 6682,
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
}),
})
);
});
test('generateExcelFile', () => {
const props = {
state: fromJS({
config: {
marketingQuestionReport: {
dateFrom: '2019-01-01',
dateTo: '2021-03-21',
},
},
}),
actions: {
clearMarketingQuestionReportDates: jest.fn(),
},
};
const tree = shallowSetup(props);
tree.instance().generateExcelFile();
tree.fecthSpy.toHaveBeenCalled();
});
});
i maked a instance of my component - i got prepaired the needed props for that call to work and i am sending them to my instance.After that i call my method - generateExcelFile();
On my coverage everything is freen except the fetch call. I don't know how to fix this. Please help
You need to create a spy of fetch before rendering the component with jest.spyOn(object, methodName):
test('generateExcelFile', () => {
const fetchSpy = jest.spyOn(window, "fetch").mockReturnValue(Promise.resolve({
blob: () =>
Promise.resolve({
size: 6682,
type:
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
}),
})
);
//...
const tree = shallowSetup(props);
tree.instance().generateExcelFile();
expect(fetchSpy).toHaveBeenCalled();
});
In your test you can check if fecthSpy have been called

Accessing the values from Promise inside useEffect hook

const useOnfidoFetch = (URL) => {
useEffect(() => {
const appToken = axios.get('http://localhost:5000/post_stuff')
.then((response) => response.data.data.data.json_data)
.then((json_data) => {
const id = json_data.applicant_id;
const token = json_data.onfido_sdk_token;
return {id, token};
});
if (appToken) {
console.log('this is working!');
OnfidoSDK.init({
// the JWT token you generated above
token: null,
containerId: "root",
steps: [
{
type: 'welcome',
options: {
title: 'Open your new bank account',
},
},
'document'
],
onComplete: function (data) {
console.log('everything is complete');
axios.post('https://third/party/api/v2/server-api/anonymous_invoke?aid=onfido_webapp', {
params: {
applicant_id: appToken.applicant_id
}
});
}
});
}
}, [URL]);
}
export default function() {
const URL = `${transmitAPI}/anonymous_invoke?aid=onfido_webapp`;
const result = useOnfidoFetch(URL, {});
return (
<div id={onfidoContainerId} />
);
}
I have refactored this dozens of times already, I am getting back some values from the appToken Promise, but I need to provide the token value from that Promise to that token property inside of Onfido.init({}) and I need to provide the id to the applicant_id property and I continue to get undefined.
If you need the token for something else as well, then i would suggest storing it in useState, and triggering OnfidoSDK.init when the value of that state changes.
Like this:
const useOnfidoFetch = (URL) => {
const [token, setToken] = useState();
useEffect(() => {
axios.get('http://localhost:5000/post_stuff')
.then((response) => response.data.data.data.json_data)
.then((json_data) => {
const token = json_data.onfido_sdk_token;
setToken(token);
})
}, [URL])
useEffect(() => {
if (!token) return;
OnfidoSDK.init({
// the JWT token you generated above
token,
containerId: "root",
steps: [
{
type: 'welcome',
options: {
title: 'Open your new bank account',
},
},
'document'
],
onComplete: function (data) {
console.log('everything is complete');
axios.post('https://third/party/api/v2/server-api/anonymous_invoke?aid=onfido_webapp', {
params: {
applicant_id: appToken.applicant_id
}
});
}
});
}, [token]);
}
Move the entire if(appToken){ ... } inside the body of the second .then((json_data) => { ... })
Something like this:
const useOnfidoFetch = (URL) => {
useEffect(() => {
const appToken = axios.get('http://localhost:5000/post_stuff')
.then((response) => response.data.data.data.json_data)
.then((json_data) => {
const id = json_data.applicant_id;
const token = json_data.onfido_sdk_token;
// Here. This code will be executed after the values are available for id and token
if (appToken) {
console.log('this is working!');
OnfidoSDK.init({
// the JWT token you generated above
token: null,
containerId: "root",
steps: [
{
type: 'welcome',
options: {
title: 'Open your new bank account',
},
},
'document'
],
onComplete: function (data) {
console.log('everything is complete');
axios.post('https://third/party/api/v2/server-api/anonymous_invoke?aid=onfido_webapp', {
params: {
applicant_id: appToken.applicant_id
}
});
}
});
}
return {id, token};
});
// In here the promise is not yet finished executing, so `id` and `token` are not yet available
}, [URL]);
};
export default function() {
const URL = `${transmitAPI}/anonymous_invoke?aid=onfido_webapp`;
const result = useOnfidoFetch(URL, {});
return (
<div id={onfidoContainerId} />
);
}
For better readability, you could also move the if(appToken){ ... } block inside a separate function that takes id, token as arguments, which you can call from inside the promise.then block.

Set State erasing previous object value React

I'm trying to make subsequent API calls to get infos about some financial indexes.. After each call, I want to update de indexObject so it has all the data from all the indexes I stored on the indexes array. The problem is, each time setIndexObject is called it overwrites the object, loosing the previous value even when i use the spread operator.
const [indexObject, setIndexObject] = useState({});
const indexes = [
"^GDAXI",
"^BVSP",
"^NDX",
"^DJI",
"^GSPC",
"^IBEX",
"^HSI",
"^BSESN",
"^FTSE",
];
useEffect(() => {
indexes.forEach((index) => {
const options = {
method: "GET",
url:
"https://apidojo-yahoo-finance-v1.p.rapidapi.com/stock/v2/get-summary",
params: { symbol: `${index}`, region: "US" },
headers: {
"x-rapidapi-key":
"----",
"x-rapidapi-host": "apidojo-yahoo-finance-v1.p.rapidapi.com",
},
};
axios
.request(options)
.then(function (response) {
setIndexObject({
...indexObject,
[index]: {
value: response.data.price.regularMarketPrice.fmt,
variation: response.data.price.regularMarketChangePercent.fmt,
},
});
})
.catch(function (error) {
console.error(error);
});
});
}, []);
This is what I am trying to achieve:
{
A:{
value:,
variation:,
},
B:{
value:,
variation,
}
}
Thanks for the help!
The problem here is you're looping the request, while the setState is not synchronous. It is better to populate all the data first, then set the state after that.
const [indexObject, setIndexObject] = useState({});
const handleFetchData = async () => {
const indexes = [
"^GDAXI",
"^BVSP",
"^NDX",
"^DJI",
"^GSPC",
"^IBEX",
"^HSI",
"^BSESN",
"^FTSE",
];
const resultData = {};
for(const index of index) {
const response = await axios.request(); // add your own request here
resultData[index] = response;
}
return resultData;
}
useEffect(() => {
handleFetchData().then(data => setIndexObject(data));
}, []);
why i use for of? because forEach or .map cannot await asynchronous loop.
Or if you want to fetch all the data simultaneously, you can consider using Promise.all

useState does not support a second callBack, what could be the easy fix?

This is my useEffect
useEffect(() => {
let pageId =
props.initialState.content[props.location.pathname.replace(/\/+?$/, "/")]
.Id;
if (props.initialState.currentContent.Url !== props.location.
setCurrentContent({ currentContent: { Name: "", Content: "" } }, () => {
fetch(`/umbraco/surface/rendercontent/byid/${pageId}`, {
credentials: "same-origin"
})
.then(response => {
if (response.ok) {
return response.json();
}
return Promise.reject(response);
})
.then(result => {
setCurrentContent({
currentContent: { Name: result.Name, Content: result.Content }
});
});
});
}
}, []);
I have tried things like useCallback/useMemo but yet no luck, I'm sure this is a simple fix but I must be missing the bigger picture, thanks in advance.
What you can do is write an effect that checks if the currentContent state is changed and empty and takes the necessary action. You would however need to ignore the initial render. Also unline setState in class components you don't pass on the state value as object instead just pass the updated state
const ContentPage = props => {
const [currentContent, setCurrentContent] = useState({
Name: props.initialState.currentContent.Name,
Content: props.initialState.currentContent.Content
});
const initialRender = useRef(true);
useEffect(() => {
let pageId =
props.initialState.content[props.location.pathname.replace(/\/+?$/,
"/")]
.Id;
if (
initialRender.current &&
currentContent.Name == "" &&
currentContent.Content == ""
) {
initialRender.current = false;
fetch(`/umbraco/surface/rendercontent/byid/${pageId}`, {
credentials: "same-origin"
})
.then(response => {
if (response.ok) {
return response.json();
}
return Promise.reject(response);
})
.then(result => {
setCurrentContent({ Name: result.Name, Content: result.Content });
});
}
}, [currentContent]);
useEffect(() => {
if (props.initialState.currentContent.Url !== props.location) {
setCurrentContent({ Name: "", Content: "" });
}
}, []);
...
};
export default ContentPage;

Resources