I am trying to add pagination in React and am sending a query for it, based on the page number of the URL. The data should be fetched and update the same component.
But the problem occurs when I go from page 2 to page 3. The data from page 2 data and page 3 data are shown alternately for a while.
After some time, the newly fetched data is updated.
I have setPost([]) after page 3 is clicked, but I don't know from where page 2 is coming.
I want to setLoading to true till the newly fetched data is completely fetched.
const Daily = ({match}) => {
const [posts, setPosts] = useState([]);
const [numberOfPages, setNumberOfPages] = useState([]);
const [loading, setLoading] = useState(true);
const perPageItems = 8;
useEffect(() => {
const totalPost = () => {
getPostsCount()
.then(data => {
setNumberOfPages(Math.ceil(data/perPageItems))
})
}
const loadIndexPosts = (a, b) => {
getPostsByIndex(a, b)
.then(data => {
if (data.error) {
console.log(data.error);
} else {
setPosts(data);
}
setLoading(false);
})
};
totalPost()
if(match.params.page==undefined)
match.params.page=1;
var n = parseInt(match.params.page)-1;
var startIdx = n*perPageItems;
loadIndexPosts(startIdx, perPageItems)
},)
console.log(posts);
return (
<div>
<div className="mt-5 daily-card-feed">
{loading && <ThreeDotsWave/>}
{posts.map((post, index) => {
return (
<div key={index}>
<DailyCard post={post}/>
</div>
)
})}
</div>
<div className="h4 pb-5 pull-right mr-5">
{match.params.page==1 &&
<Link to={`/daily/page/2`} onClick={() => {setLoading(true); setPosts([])}}>Older Post -></Link>}
{match.params.page!=1 &&
<Link to={`${parseInt(match.params.page)+1}`} onClick={() => {setLoading(true); setPosts([])}}>Older Post -></Link>}
</div>
</div>
)
}
First of all, let's figure out the logic of loading new data. When a new page arrives at path, you want to call the api. I think the problem is here:
useEffect(() => {
const totalPost = () => {
getPostsCount()
.then(data => {
setNumberOfPages(Math.ceil(data/perPageItems))
})
}
const loadIndexPosts = (a, b) => {
getPostsByIndex(a, b)
.then(data => {
if (data.error) {
console.log(data.error);
} else {
setPosts(data);
}
setLoading(false);
})
};
totalPost()
if(match.params.page==undefined)
match.params.page=1;
var n = parseInt(match.params.page)-1;
var startIdx = n*perPageItems;
loadIndexPosts(startIdx, perPageItems)
},)
We can break this like:
const totalPost = () => {
getPostsCount()
.then(data => {
setNumberOfPages(Math.ceil(data/perPageItems))
})
}
const loadIndexPosts = (a, b) => {
getPostsByIndex(a, b)
.then(data => {
if (data.error) {
console.log(data.error);
} else {
setPosts(data);
}
setLoading(false);
})
};
useEffect(() => {
if(match.params.page==undefined)
match.params.page=1;
var n = parseInt(match.params.page)-1;
var startIdx = n*perPageItems;
loadIndexPosts(startIdx, perPageItems)
},[numberOfPages])
This will add the dependency of the call when page number changes. And call totalPost() only from click event.
Related
import {useState, useEffect } from 'react'
import axios from 'axios'
const Singlecountry = ({searchedCountries, setWeather, weather}) => {
const weatherName = searchedCountries[0].capital
const iconname = () => {
if (weather === undefined) {
return null
}
weather.map(w => w.weather[0].icon)
}
console.log(iconname)
useEffect(() => {
axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${weatherName}&appid=${process.env.REACT_APP_API_KEY}`)
.then(response => {
const apiResponse = response.data;
console.log(apiResponse)
console.log(`Current temperature in ${apiResponse.name} is ${apiResponse.main.temp - 273.15}℃`);
setWeather([apiResponse])
}).catch(error => {
console.log(error);
})
}, [])
return(
<div>
capital: {searchedCountries.map(c => <p>{c.capital}</p>)}
area: {searchedCountries.map(c => <p>{c.area}</p>)}
<h2>Languages</h2>
<ul>
{
searchedCountries.map(c =>
<ul>
{Object.values(c.languages).map(l => <li>{l}</li>)}
</ul>
)
}
</ul>
{searchedCountries.map(c => <img src={Object.values(c.flags)[0]} alt="" /> )}
<h3>Weather</h3>
<p>temperature is {weather.map(w => w.main.temp - 273.15)} degrees Celsius</p>
<p>wind is {weather.map(w => w.wind.speed)} miles per hour</p>
<img src={`http://openweathermap.org/img/wn/${iconname}.png`} alt="" />
</div>
)
}
const Countries = ({ searchedCountries, handleClick, show, setWeather, setCountries, weather}) => {
if (weather === undefined) {
return null
}
if (searchedCountries.length >= 10) {
return (
<div>
<p>too many countries to list, please narrow your search</p>
</div>
)
}
if (searchedCountries.length === 1) {
return (
<Singlecountry searchedCountries={searchedCountries} setWeather={setWeather} weather={weather}/>
)
}
if (show === true) {
return (
<Singlecountry searchedCountries={searchedCountries} setWeather={setWeather} />
)
}
return (
<ul>
{searchedCountries.map(c => <li>{c.name.common}<button onClick={handleClick} >show</button></li>)}
</ul>
)
}
const App = () => {
const [countries, setCountries] = useState([])
const [newSearch, setNewSearch] = useState('')
const [show, setShow] = useState(false)
const [weather, setWeather] = useState('')
const handleSearchChange = (event) => {
setNewSearch(event.target.value)
}
const handleClick = () => {
setShow(!show)
}
const searchedCountries =
countries.filter(c => c.name.common.includes(newSearch))
useEffect(() => {
axios
.get('https://restcountries.com/v3.1/all')
.then(response => {
setCountries(response.data)
})
}, [])
return (
<div>
<div><p>find countries</p><input value={newSearch} onChange={handleSearchChange} /></div>
<div>
<h2>countries</h2>
<Countries searchedCountries={searchedCountries} handleClick={handleClick} show={show} setCountries={setCountries} setWeather={setWeather} weather={weather}/>
</div>
</div>
)
}
export default App
The following code is designed to display information on countries when the user types in the countries' name in the search bar, including capital city, temperature and its weather.
The app fetches country data from a Countries API and when the user searches for a specific country, the weather its then fetched from a Weather API.
However, when the app is refreshed, the app breaks when searching for an individual country's weather.
Does anyone know why this is and how to solve it?
Thanks
It looks like you're using axios inside useEffect which can cause and infinite loop and crash your app. I recommend creating a separate function for your data fetching and then call the function in the useEffect like so:
const fetchCountries = useCallback(() => {
axios
.get('https://restcountries.com/v3.1/all')
.then(response => {
setCountries(response.data)
})
}, [])
useEffect(() => {
fetchCountries()
}, [fetchCountries])
The key is the dependency array in useEffect which will only update if there is a change in the list of countries from fetchCountries function, thus preventing the infinite loop.
This question already has answers here:
How do I return the response from an asynchronous call?
(41 answers)
Closed 1 year ago.
I fetched an array of products from firebase with the normal way :
export const getUsersProducts = async uid => {
const UsersProducts = []
await db.collection('products').where("userID", "==", uid).get().then(snapshot => {
snapshot.forEach(doc => UsersProducts.push(doc.data()))
})
return UsersProducts
}
and the fetched array shows up in the dom normally, but when I tried to fetch it with onSnapshot method it didnt show up on the DOM even though in appeared in my redux store and when I console log it, it shows up normally.
export const getUsersProducts = uid => {
let UsersProducts = []
db.collection('products').where("userID", "==", uid).onSnapshot(querySnapshot => {
querySnapshot.docChanges().forEach(change => {
if (change.type === "added") {
UsersProducts.push(change.doc.data())
}
})
})
return UsersProducts
}
here is the code used to show it in the DOM
const MyProducts = () => {
const CurrentUserInfos = useSelector(state => state.userReducer.currentUserInfos)
const searchQuery = useSelector(state => state.productsReducer.searchQuery)
const myProducts = useSelector(state => state.productsReducer.usersProducts)
const dispatch = useDispatch()
const settingUsersProductList = async () => {
try {
const usersProducts = getUsersProducts(CurrentUserInfos.userID)
dispatch(setUsersProducts(usersProducts))
console.log(myProducts)
} catch (err) {
console.log(err)
}
}
useEffect(() => {
settingUsersProductList()
}, [CurrentUserInfos])
return (
<div className="my-products">
<div className="my-products__search-bar">
<SearchBar />
</div>
<div className="my-products__list">
{
Object.keys(myProducts).length===0 ? (<Loading />) : (myProducts.filter(product => {
if(searchQuery==="")
return product
else if(product.title && product.title.toLowerCase().includes(searchQuery.toLowerCase()))
return product
}).map(product => {
return(
<ProductItem
key={product.id}
product={product}
/>
)
}))
}
</div>
</div>
)
}
export default MyProducts
You are returning the array before promise is resolved hence its empty. Try this:
export const getUsersProducts = async uid => {
const snapshot = await db.collection('products').where("userID", "==", uid).get()
const UsersProducts = snapshot.docs.map(doc => doc.data())
return UsersProducts
}
For onSnapshot, add the return statement inside of onSnapshot,
export const getUsersProducts = uid => {
let UsersProducts = []
return db.collection('products').where("userID", "==", uid).onSnapshot(querySnapshot => {
querySnapshot.docChanges().forEach(change => {
if (change.type === "added") {
UsersProducts.push(change.doc.data())
}
})
return UsersProducts
})
}
I'm new to React. I'm trying to make my socket io listener work. When I it out of useEffect it works but it is called several times. In useEffect it is called only once (which is good obviously) but this time users are not updated - initial value.
function Users() {
const [users, setUsers] = useState([]);
useEffect(() => {
getUsers();
if(users.length > 0) {
socket.on("statusChange", (data) => {
console.log(users); // this returns initial state of users
let tempUsers = [...users];
let ndx = tempUsers.findIndex(obj => obj.id === data.id);
if(ndx === -1)
return;
tempUsers[ndx].status = data.status;
setUsers(tempUsers);
});
}
}, []);
function getUsers() {
fetch(/* stuff */)
.then(res => res.json())
.then(res => setUsers(res.data));
}
return (
<div className={styles.container}>
<div className={styles.usersContainer}>
{ users.map((user, i) => <UserCard key={i} user={user} />) }
</div>
</div>
);
}
export default Users;
Some parts where I'm making assumptions:
getUsers() should be called only once: when the component mounts
We want to listen to socket.on("statusChange") and get updates about users we got from getUsers().
// This is pulled outside the component.
function getUsers() {
fetch(/* stuff */)
.then(res => res.json());
}
function Users() {
const [users, setUsers] = useState([]);
// Fetch users only once, when the component mounts.
useEffect(() => {
getUsers().then(res => {
setUsers(res.data);
});
}, []);
// Listen to changes
useEffect(() => {
if (!users.length) return;
const listener = (data) => {
// Functional update
// See: https://reactjs.org/docs/hooks-reference.html#functional-updates
setUsers(prevUsers => {
let nextUsers = [...prevUsers];
let ndx = nextUsers.findIndex(obj => obj.id === data.id);
// What's up here? See below.
if(ndx === -1) return nextUsers;
nextUsers[ndx].status = data.status;
return nextUsers;
});
};
socket.on("statusChange", listener);
// Unsubscribe when this component unmounts
// See: https://reactjs.org/docs/hooks-effect.html#example-using-hooks-1
return () => socket.off("details", listener);
}, [users]);
return (
<div className={styles.container}>
<div className={styles.usersContainer}>
{ users.map((user, i) => <UserCard key={i} user={user} />) }
</div>
</div>
);
}
export default Users;
About if(ndx === -1) return nextUsers;
This means that users will never change in size, i.e. you won't handle data about a new user.
Alternatively, you could do if(ndx === -1) return [ ...nextUsers, data ];
Instead of simply executing getUsers, use it with a callback. And then in the callBack execute setUsers:
function Users() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch(/* stuff */)
.then(res => res.json())
.then(res => {
if(res.data.length > 0) {
socket.on("statusChange", (data) => {
console.log(res.data); // this returns initial state of users
let tempUsers = [...res.data];
let ndx = tempUsers.findIndex(obj => obj.id === data.id);
if(ndx === -1)
return;
tempUsers[ndx].status = data.status;
setUsers(tempUsers);
});
}
});
}, []);
return (
<div className={styles.container}>
<div className={styles.usersContainer}>
{ users.map((user, i) => <UserCard key={i} user={user} />) }
</div>
</div>
);
}
export default Users;
I got stuck with the following and haven't found any answer after a lot of research.
What I want to do: simply getting users inluding their images from a firestore-DB with react and the useeffect-hook and displaying them.
The DB-structure looks as follows:
https://i.stack.imgur.com/sDcrv.png
So the pictures are a subcollection of the users-collection.
After getting the users from the users-collection, I'm doing a second request for adding the users images to this specific user using Object.assign. After every forEach-run over the users-collection I'm setting the users-array with setUsers((oldUsers) => [...oldUsers, currentUser]);. Logging the users-array shows uses INCLUDING their images.
The problem: When trying to render the images, they are always undefined.
Workaround: Pressing a button that calls a function for re-setting the users:
const reRenderUsers = () => {
if (userDataLoaded === false) {
setUserDataLoaded(true);
}
const copy = [...users];
setUsers(copy);
};
^ This solves the problem and all images where shown.
Question: Is there any possibility showing the images instantly without the need of "re-rendering" the users? Am I using the useEffect-hook wrong for example? I'm thankful for any advice. Many thanks in advance!
Here the full code:
const [users, setUsers] = useState([]);
const [userDataLoaded, setUserDataLoaded] = useState(false);
useEffect(() => {
const unsubscribe = database.collection("users").onSnapshot((snapshot) => {
snapshot.forEach((doc) => {
const currentUser = {
id: doc.id,
...doc.data(),
};
database
.collection("users")
.doc(currentUser.id)
.collection("pictures")
.get()
.then((response) => {
const fetchedPictures = [];
response.forEach((document) => {
const fetchedPicture = {
id: document.id,
...document.data(),
};
fetchedPictures.push(fetchedPicture);
});
currentUser.pictures = [];
Object.assign(currentUser.pictures, fetchedPictures);
})
.catch((error) => {
console.log(error);
});
setUsers((oldUsers) => [...oldUsers, currentUser]);
});
});
return () => {
unsubscribe();
};
}, []);
const reRenderUsers = () => {
if (userDataLoaded === false) {
setUserDataLoaded(true);
}
const copy = [...users];
setUsers(copy);
};
return (
<div>
{!userDataLoaded ? (
<button onClick={reRenderUsers}> load users </button>
) : null}
{users.map((user, index) => (
<div key={user.id}>
{user.pictures && <img src={user.pictures[0].imageUrl}></img>}
</div>
))}
</div>
);
}
export default User;
This is because you are calling setUser before the firebase response completes the callback chain. You need to update the state right after the loop inside the success callback completed. I have updated useEffect to update it right after the callback
useEffect(() => {
const unsubscribe = database.collection("users").onSnapshot((snapshot) => {
snapshot.forEach((doc) => {
const currentUser = {
id: doc.id,
...doc.data(),
};
database
.collection("users")
.doc(currentUser.id)
.collection("pictures")
.get()
.then((response) => {
const fetchedPictures = [];
response.forEach((document) => {
const fetchedPicture = {
id: document.id,
...document.data(),
};
fetchedPictures.push(fetchedPicture);
});
currentUser.pictures = fetchedPictures;
setUsers((oldUsers) => [...oldUsers, currentUser]);
})
.catch((error) => {
console.log(error);
});
//dont need this here
//setUsers((oldUsers) => [...oldUsers, currentUser]);
});
});
return () => {
unsubscribe();
};
}, []);
Good Luck
I am trying to let the FlatList get 20 posts from Firestore and render 20. when the end is reached I would like to call the getPosts method to get the next 20 posts which means I will have to have a way to save the last known cursor. This is what I was trying to do when converting class component to hooks.
Please can someone help me , no one answered my last question about this
const Posts = (props) => {
//How to get 20 posts from firebase and then render 20 more when the end is reached
const [allPosts, setAllPosts] = useState();
const [loading, setLoading] = useState(true)
const [isRefreshing, setRefreshing] = useState(false);
useEffect(() => {
getPosts();
}, []);
const getPosts = async () => {
try {
var all = [];
const unsubscribe = await firebase
.firestore()
.collection("Posts")
.orderBy("timestamp",'desc')
.get()
.then((querySnapshot) => {
querySnapshot.docs.forEach((doc) => {
all.push(doc.data());
});
setLoading(false);
});
setAllPosts(all);
if(currentUser === null){
unsubscribe()
}
} catch (err) {
setLoading(false);
}
};
const onRefresh = useCallback(() => {
setRefreshing(true);
getPosts()
.then(() => {
setRefreshing(false);
})
.catch((error) => {
setRefreshing(false); // false isRefreshing flag for disable pull to refresh
Alert.alert("An error occured", "Please try again later");
});
}, []);
return (
<FlatList
data={allRecipes}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={onRefresh}
/>
}
initialNumToRender={20}
keyExtractor={(item, index) => item.postId}
renderItem={renderItem}
/>
);
}
const Posts = () =>{
const [posts, setPosts] = useState();
const [data, setData] = useState();
const addPosts = posts => {
setData({...data,...posts})
// `setData` is async , use posts directly
setPosts(Object.values(posts).sort((a, b) => a.timestamp < b.timestamp))
};
}
You need to add a scroll event listener here
something like:
const Posts = (props) => {
useEffect(() => {
window.addEventListener('scroll', () => {
if (window.scrollY >= (document.body.offsetHeight + window.innerHeight)) {
// fetch more posts here
}
});
});
// ...rest of the codes
}