How to set value of react hook in promise? - javascript

I am getting a data from the promise and I want to set it in react hook, it does but it makes infinite requests and re-rendering of the page, also I want some specific only data to fill
const [rows, setRows] = useState([]);
useEffect(() => {
async function fetchData() {
myEmploiList({
user: user.id
}).then((data) => {
//setRows(data);
const newData = [];
data.forEach((item, index) => {
newData[index] = {
id: item._id,
name: item.name,
society: item.society
};
setRows(newData);
});
});
}
fetchData();
});

You should add dependencies to your useEffect hook. It is the second argument of this hook.
useEffect(() => {
// your code
}, [deps]);
deps explanation:
no value: will execute effect every time your component renders.
[]: will execute effect only the first time the component renders.
[value1, value2, ...]: will execute effect if any value changes.
For further reading, I highly recommend this blog post.

Move setRows call out of the forEach loop and include user.id into the dependency array
const [rows, setRows] = useState([]);
useEffect(() => {
async function fetchData() {
myEmploiList({
user: user.id
}).then((data) => {
//setRows(data);
const newData = [];
data.forEach((item, index) => {
newData[index] = {
id: item._id,
name: item.name,
society: item.society
};
});
setRows(newData);
});
}
fetchData();
}, [user.id]);

Related

Why getting too many rerenders in react?

I have the following code in React to get data from Firebase. I am new to useEffect and it is giving too many rerenders error:
let post1 = [];
useEffect(() => {
getDocs(collection(db, "posts")).then((snapshot) => {
const data = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
post1 = data;
})
}, [posts]);
setPosts(post1);
The way you wrote it, setPosts(post1) is being called on every render. I'm assuming posts and setPosts are destructured from a useState() value which means that every time you call setPosts(), it triggers a rerender. You need to move the setPosts() call to the useEffect(). You also need to remove posts from the dependency array of useEffect because if any of those dependencies change, it triggers a rerender as well. In your specific case, try this:
useEffect(() => {
getDocs(collection(db, "posts")).then((snapshot) => {
const data = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
setPosts(data);
})
}, []);

how to stop multiple re-renders from doing multiple api calls useEffect?

I'm new to react functional components and I'm trying to get the weather data on multiple cities on page load but useEffect is now re-rending each call. How can I write this so useEffect doesn't cause re-renders?
function App() {
const [data, setData] = useState([]);
const [activeWeather, setActiveWeather] = useState([]);
useEffect(() => {
const key = process.env.REACT_APP_API_KEY;
const fetchData = async (city) => {
const res = await axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${key}`);
setData((data) => [
...data,
{ description: res.data.weather[0].description, icon: res.data.weather[0].icon, temp: res.data.main.temp, city: res.data.name, country: res.data.sys.country, id: res.data.id },
]);
};
const fetchCities = () => {
const cities = [fetchData("Ottawa"), fetchData("Toronto"), fetchData("Vancouver"), fetchData("California"), fetchData("London")];
Promise.all(cities).catch((err) => {
console.log(err);
});
};
fetchCities();
}, []);
You can make the fetchData function to return the data you need without updating the state, then you can fetch x amount of cities and only when all of the requests complete update the state.
Note that if one of the requests inside Promise.all fail, it will go to the catch block without returning any data back, basically all or nothing
const key = process.env.REACT_APP_API_KEY
const fetchCity = async city => {
const { data } = await axios.get(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${key}`,
)
return {
description: data.weather[0].description,
icon: data.weather[0].icon,
temp: data.main.temp,
city: data.name,
country: data.sys.country,
id: data.id,
}
}
function App() {
const [cities, setCities] = useState([])
const [activeWeather, setActiveWeather] = useState([])
useEffect(() => {
const fetchCities = async () => {
const citiesData = await Promise.all(
['Ottawa', 'Toronto', 'Vancouver'].map(fetchCity),
)
setCities(prevState => prevState.concat(citiesData))
}
fetchCities()
}, [])
}
You can use Promise.all and then call setData once. something like this:
useEffect(() => {
const fetchCity = (city) => axios.get(`${base}/${city}`);
const cities = ["Ottawa", "Toronto"];
const promises = cities.map(fetchCity);
Promise.all(promises).then((responses) => {
setData(cities.map((city, index) => ({ city, ...responses[index] })));
});
}, []);

React Hook useEffect has a missing dependency: 'getContacts'

Before posting the below, I have reviewed similar posts on stackoverflow but none resolved my issue.
I'm new to react and fetching data from firestore database. The below code works as required but getting this prompt within react
import React, {useState, useEffect} from 'react'
import {db} from '../firebase'
const ListRecord = () => {
const [details, setDetails] = useState([]);
useEffect(() => {
getContacts()
},[]);
const getContacts = async() => {
await db.collection('contacts').get().then((querySnapshot) => {
let arr = []
querySnapshot.forEach((doc) => {
arr.push({id: doc.id, value: doc.data()})
});
setDetails(arr);
});
console.log(details);
return details
}
return (
<div>
<h2>List Contact Details</h2>
</div>
)
}
export default ListRecord
As per other similar posts I tried moving the getContacts function inside useEffect body which make the prompt disapper but the getContacts function goes in a continuous loop.
I'm not sure what I'm missing here and any help would be appreciated.
There are different potential solutions:
1. Move getContacts() inside the useEffect() hook:
If you call getContacts() only once and only when the component mounts for the first time, this is probably the most logic solution.
useEffect(() => {
const getContacts = async () => {
await db.collection('contacts').get().then((querySnapshot) => {
let arr = []
querySnapshot.forEach((doc) => {
arr.push({
id: doc.id,
value: doc.data()
})
});
setDetails(arr);
});
//console.log(details);
//return details // why are you returning details?
}
getContacts()
}, [setDetails]); // setDetails() is granted to never change therefore the hook will never re-run
or, of course, you can use an IIFE:
useEffect(() => {
(async function() {
// ... same body as getContacts
})()
}, [setDetails])
2. Use a useCallback() hook:
This is something you might want to do if getContacts() is called more than once (for example, when the component mounts and every time some prop changes or when you click on some button)
const getContacts = useCallback(async () => {
await db.collection('contacts').get().then((querySnapshot) => {
let arr = []
querySnapshot.forEach((doc) => {
arr.push({
id: doc.id,
value: doc.data()
})
});
setDetails(arr);
});
//console.log(details);
//return details // why are you returning details?
}, [setDetail]); // setDetails() is granted to never change therefore getContacts() will never be re-created
useEffect(() => {
getContacts()
}, [getContacts]); // as getContacts() never changes, this will run only once
3. Move getContacts() out of the component and make it an independent function:
This can make sense if you want to reuse the same logic into other components:
// getContacts.js file
// OR this code could be in the ListRecord.js file but **outside** the component,
// although, in this case, solutions (1) or (2) would make more sense
import { db } from 'path/to/firebase'
export async function getContacts() {
await db.collection('contacts').get().then((querySnapshot) => {
let arr = []
querySnapshot.forEach((doc) => {
arr.push({
id: doc.id,
value: doc.data()
})
});
return arr; // this time you HAVE TO return arr
});
}
// ListRecord.js file
import React, { useState, useEffect } from 'react';
import { getContacts } from 'path/to/getContacts.js';
const ListRecord = () => {
const [details, setDetails] = useState([]);
useEffect(async () => {
const arr = await getContacts();
if (arr && arr.length > 0) setDetails(arr);
}, [setDetails]);
//...
}
I suggest you have a look at how useEffect and its dependency list works in the official document.
In short, do the following:
useEffect(() => {
getContacts()
}, [getContacts]);
This means when getContacts changes, the useEffect will be re-run.

Re-fetching with useQuery on argument change

I am trying to implement pagination using react-query. On page change I am updating the page inside useEffect using setArrivals. For some reason I am always sending the previous value of the arrivals as the argument for the getProductList function. To fix the issue I am sending the refetch() request inside the setTimeout. It does work but it doesn't feel right to me. Let me know what I am doing wrong.
const HomePage = ({ newArrivals }) => {
const [page, setPage] = useState(1);
const [arrivals, setArrivals] = useState({ ...newArrivals, page: page });
useEffect(() => {
setArrivals((values) => {
console.log({ page });
return { ...values, page: page };
});
setTimeout(function () {
newArrivalsQuery.refetch();
}, 0);
}, [page]);
const newArrivalsQuery = useQuery(
['productListByNewArrivals'],
() => getProductList(arrivals),
{
select: useCallback((data) => {
return JSON.parse(data);
}, []),
}
);
return (
<>
<NewArrivals
newArrivalsQuery={newArrivalsQuery}
page={page}
setPage={setPage}
/>
</>
);
};
export async function getServerSideProps(context) {
const newArrivals = {
sort: 'createdAt',
order: 'desc',
};
try {
const queryClient = new QueryClient();
await queryClient.prefetchQuery('productListByNewArrivals', async () => {
const newArrivalsResult = await listProduct(newArrivals);
return JSON.stringify(newArrivalsResult);
});
return {
props: {
newArrivals: newArrivals,
dehydratedState: dehydrate(queryClient),
},
};
} catch (error) {
console.log('error: ', error);
if (error) {
return {
redirect: {
destination: '/login',
permanent: false,
},
};
}
}
}
The best way is to add the dependencies of your query function to the queryKey. react-query is declarative and will re-fetch automatically if the queryKey changes. If you have to reach to useEffect and refetch, it's likely not the easiest solution:
const HomePage = ({ newArrivals }) => {
const [page, setPage] = useState(1);
const [arrivals, setArrivals] = useState({ ...newArrivals, page: page });
const newArrivalsQuery = useQuery(
['productListByNewArrivals', arrivals],
() => getProductList(arrivals),
{
select: useCallback((data) => {
return JSON.parse(data);
}, []),
}
);
now, arrivals are part of the queryKey, which is what you are using in the queryFn in getProductList. Now all you need to do is call setArrivals and react-query will refetch.
Side note: it looks like arrivals is not really state, but derived state. At least in this snippet, you only call the setter in an effect, which seems wrong. It looks like you want to keep in in-sync with page and compute it every time you call setPage, so you can also do:
const [page, setPage] = useState(1);
const arrivals = { ...newArrivals, page: page };

Testing child component props after parent update state using set

I have a following functional component which send some filtered data to the child component. Code is working fine i.e I can run app and see the components being render with right data. But the test I have written below is failing for ChildComponent. Instead of getting single array element with filtered value it is getting all three original values.
I am confused as similar test for FilterInputBox component for props filterValue is passing. Both tests are checking the updated props value after same event filter input change i.e handleFilterChange.
Am I missing anything? Any suggestion?
Source Code
function RootPage(props) {
const [filterValue, setFilterValue] = useState(undefined);
const [originalData, setOriginalData] = useState(undefined);
const [filteredData, setFilteredData] = useState(undefined);
const doFilter = () => {
// do something and return some value
}
const handleFilterChange = (value) => {
const filteredData = originalData && originalData.filter(doFilter);
setFilteredData(filteredData);
setFilterValue(value);
};
React.useEffect(() => {
async function fetchData() {
await myService.fetchOriginalData()
.then((res) => {
setOriginalData(res);
})
}
fetchData();
}, [props.someFlag]);
return (
<>
<FilterInputBox
filterValue={filterValue}
onChange={handleFilterChange}
/>
<ChildComponent
data={filteredData}
/>
</>
);
}
Test Code
describe('RootPage', () => {
let props,
fetchOriginalDataStub,
useEffectStub,
originalData;
const flushPromises = () => new Promise((resolve) => setImmediate(resolve));
beforeEach(() => {
originalData = [
{ name: 'Brown Fox' },
{ name: 'Lazy Dog' },
{ name: 'Mad Monkey' }
];
fetchOriginalDataStub = sinon.stub(myService, 'fetchOriginalData').resolves(originalData);
useEffectStub = sinon.stub(React, 'useEffect');
useEffectStub.onCall(0).callsFake((f) => f());
props = { ... };
});
afterEach(() => {
sinon.restore();
});
it('should send filtered data', async () => {
const renderedElement = enzyme.shallow(<RootPage {...props}/>);
const filterBoxElement = renderedElement.find(FilterInputBox);;
await flushPromises();
filterBoxElement.props().onChange('Lazy');
await flushPromises();
//This "filterValue" test is passing
const filterBoxWithNewValue = renderedElement.find(FilterInputBox);
expect(filterBoxWithNewValue.props().filterValue).to.equal('Lazy');
//This "data" test is failing
const childElement = renderedElement.find(ChildComponent);
expect(childElement.props()).to.eql({
data: [
{ name: 'Lazy Dog' }
]
});
});
});
UPDATE After putting some log statements I am seeing that when I am calling onChange originalData is coming undefined. Not sure why that is happening that seems to be the issue.
Still looking for help if anyone have any insight on this.

Categories

Resources