I am developing a MERN stack app where I am trying to implement server side pagination.
Note that I am using React Query for managing server state.
On the root route, I display two posts.
When I click on the Next button, NEXT two blog posts should be displayed. However, this is not happening. I see the same two posts on page 2 as on page 1.
Where is the problem?
I think, there is either some problem with my server side pagination logic or with React Query. I suspect React Query is not fetching blog posts when I click on the Next button; instead it is fetching the posts from the cache. (I could be wrong here).
These are my code snippets:
postControllers.js
const asyncHandler = require("express-async-handler");
const Post = require("../models/postModel");
const fetchAllPosts = asyncHandler(async (req, res) => {
const { pageNumber } = req.body;
const pageSize = 2;
const postCount = await Post.countDocuments({});
const pageCount = Math.ceil(postCount / pageSize);
const posts = await Post.find({})
.limit(2)
.skip(pageSize * (pageNumber - 1));
res.json({ posts, pageCount });
});
postRoutes.js
const express = require("express");
const { fetchAllPosts, createPost } = require("../controllers/postControllers");
const router = express.Router();
router.get("/api/posts", fetchAllPosts);
module.exports = router;
Posts.js
import React, { useState } from "react";
import { useFetchAllPosts } from "../hooks/postsHooks";
import Spinner from "../sharedUi/Spinner";
const Posts = () => {
const [pageNumber, setPageNumber] = useState(1);
const { data, error, isLoading, isError } = useFetchAllPosts(pageNumber);
const handlePrevious = () => {
setPageNumber((prevPageNum) => prevPageNum - 1);
};
const handleNext = () => {
setPageNumber((prevPageNum) => prevPageNum + 1);
};
return (
<div>
{isLoading ? (
<Spinner />
) : isError ? (
<p>{error.message}</p>
) : (
data.posts.map((post) => (
<p className="m-6" key={post.title}>
{post.title}
</p>
))
)}
<div>
{isLoading ? (
<Spinner />
) : isError ? (
<p>{error.message}</p>
) : (
<div className="flex justify-between m-6 w-60">
<button
disabled={pageNumber === 1}
className="bg-blue-400 px-1 py-0.5 text-white rounded"
onClick={handlePrevious}
>
Previous
</button>
<p>{data.pageCount && `Page ${pageNumber} of ${data.pageCount}`}</p>
<button
disabled={pageNumber === data.pageCount}
className="bg-blue-400 px-1 py-0.5 text-white rounded"
onClick={handleNext}
>
Next
</button>
</div>
)}
</div>
</div>
);
};
export default Posts;
postHooks.js
import { useQuery, useMutation, useQueryClient } from "#tanstack/react-query";
import { useNavigate } from "react-router-dom";
import axios from "axios";
export const useFetchAllPosts = (pageNumber) => {
const response = useQuery(["posts"], async () => {
const { data } = await axios.get("/api/posts", pageNumber);
return data;
});
return response;
};
all dependencies of a query (= everything that is used inside the query function) needs to be part of the query key. React Query will only trigger auto fetches when the key changes:
export const useFetchAllPosts = (pageNumber) => {
const response = useQuery(["posts", pageNumber], async () => {
const { data } = await axios.get("/api/posts", pageNumber);
return data;
});
return response;
};
to avoid hard loading states between pagination, you might want to set keepPreviousData: true
see also:
the official pagination example
the guide on paginated queries
Related
I Use next 13 , I wanna add AOS library in my app and us it , I installed it , and added it properly to my component, but my component doesn't show and also I don't get any error.here is my Home Page code :
import Hero from "../components/index/Hero"
import ItSolutions from "#/components/index/ItSolutions"
import Skills from "#/components/index/Skills"
import Script from "next/script"
import AOS from 'aos';
import 'aos/dist/aos.css';
const getData = async (type) => {
const projectId = 'q8l0xi0c'
const dataset = 'production'
const query = encodeURIComponent(`*[_type == "${type}"]`)
const url = `https://${projectId}.api.sanity.io/v2021-10-21/data/query/${dataset}?
query=${query}`
const response = await fetch(url)
const data = (await response.json()).result
return data
}
const Home = async () => {
<Script>
AOS.init();
</Script>
const members = await getData('member')
const blogs = await getData('blog')
const customers = await getData('customer')
const solutions = await getData('solution')
return (
<div>
<Hero />
<ItSolutions solutions={solutions} />
<Skills />
</div>
)
}
export default Home
and I used this div for another component named Skills to apply AOS :
<div className="max-w-screen-xl mt-28 pt-36 px-4 pb-28 sm:px-16 md:px-8 lg:grid lg:grid-cols-2
lg:gap-20 lg:mt-16 xl:px-20 xl:mx-auto " data-aos="fade-up"> HI There
</div>
I am working on a real estate project, and use React for the frontend, Firebase as the backend, I got a problem with the category page below:
There are two different categories of this project, one for sale, and the other one for rent, when I click each category, we will be redirected to the corresponding page ( page of sale OR rent listings );
Every time when I click and enter the page of sale listings, it shows "No listing for sale" first (please see the screenshot), after I refreshed this page manually, then the sale listings came out.
The first time when I enter the page of sale listings:
##After I refreshed this page manually:
But when I click and enter the page of rental listings, everything works well.
Can anyone help me with this? I really appreciate it.
#category.jsx:
import React, { useEffect, useState, useCallback } from "react";
import { useParams } from "react-router-dom";
import {
collection,
getDocs,
query,
where,
orderBy,
limit,
startAfter,
} from "firebase/firestore";
import { db } from "../firebase.config";
import { toast } from "react-toastify";
import Spinner from "../components/Spinner";
import ListingItem from "../components/ListingItem";
function Category(props) {
const [listings, setListings] = useState(null);
const [loading, setLoading] = useState(true);
const [lastFetchedListing, setLastFetchedListing] = useState(null);
const params = useParams();
const fetchListings = useCallback(async () => {
try {
const listingsRef = collection(db, "listings");
const q = query(
listingsRef,
where("type", "==", params.categoryName),
orderBy("timestamp", "desc"),
limit(10)
);
// Execute query
const querySnap = await getDocs(q);
const lastVisible = querySnap.docs[querySnap.docs.length - 1];
setLastFetchedListing(lastVisible);
const listings = [];
querySnap.forEach((doc) => {
return listings.push({
id: doc.id,
data: doc.data(),
});
});
setListings(listings);
setLoading(false);
} catch (error) {
toast.error("Could not fetch listings");
}
}, [params.categoryName]);
useEffect(() => {
fetchListings();
}, [fetchListings]);
const onFetchMoreListings = async () => {
try {
const listingsRef = collection(db, "listings");
const q = query(
listingsRef,
where("type", "==", params.categoryName),
orderBy("timestamp", "desc"),
startAfter(lastFetchedListing),
limit(10)
);
const querySnap = await getDocs(q);
const lastVisible = querySnap.docs[querySnap.docs.length - 1];
setLastFetchedListing(lastVisible);
const listings = [];
querySnap.forEach((doc) => {
return listings.push({
id: doc.id,
data: doc.data(),
});
});
setListings((prevState) => [...prevState, ...listings]);
setLoading(false);
} catch (error) {
toast.error("Could not fetch listings");
}
};
return (
<div className="category container">
<p className="pageHeader">
{params.categoryName === "rent" ? "Places for rent" : "Places for sale"}
</p>
{loading ? (
<Spinner />
) : listings && listings.length > 0 ? (
<>
<div>
<ul className="">
{listings.map((listing) => (
<ListingItem
listing={listing.data}
id={listing.id}
key={listing.id}
/>
))}
</ul>
</div>
{lastFetchedListing && (
<p className="loadMore" onClick={onFetchMoreListings}>
Load More
</p>
)}
</>
) : (
<p>No listings for {params.categoryName}</p>
)}
</div>
);
}
export default Category;
I am learning React, and trying to build a photo Album with a a modal slider displaying the image clicked (on a different component) in the first place.
To get that, I set <img src={albums[slideIndex].url} /> dynamically and set slideIndex with the idof the imgclicked , so the first image displayed in the modal slider is the one I clicked.
The problem is that before I click in any image albums[slideIndex].urlis obviously undefined and I get a TypeError :cannot read properties of undefined
How could I solve that?
I tried with data checks with ternary operator, like albums ? albums[slideIndex].url : "no data", but doesn't solve it.
Any Ideas? what i am missing?
this is the component where I have the issue:
import React, { useContext, useEffect, useState } from "react";
import { AlbumContext } from "../../context/AlbumContext";
import AlbumImage from "../albumImage/AlbumImage";
import "./album.css";
import BtnSlider from "../carousel/BtnSlider";
function Album() {
const { albums, getData, modal, setModal, clickedImg } =
useContext(AlbumContext);
console.log("clickedImg id >>", clickedImg.id);
useEffect(() => {
getData(); //-> triggers fetch function on render
}, []);
///////////
//* Slider Controls
///////////
const [slideIndex, setSlideIndex] = useState(clickedImg.id);
console.log("SlideINDEx", slideIndex ? slideIndex : "no hay");
const nextSlide = () => {
if (slideIndex !== albums.length) {
setSlideIndex(slideIndex + 1);
} else if (slideIndex === albums.length) {
setSlideIndex(1);
}
console.log("nextSlide");
};
const prevSlide = () => {
console.log("PrevSlide");
};
const handleOnclick = () => {
setModal(false);
console.log(modal);
};
return (
<div className="Album_Wrapper">
<div className={modal ? "modal open" : "modal"}>
<div>
<img src={albums[slideIndex].url} alt="" />
<button className="carousel-close-btn" onClick={handleOnclick}>
close modal
</button>
<BtnSlider moveSlide={nextSlide} direction={"next"} />
<BtnSlider moveSlide={prevSlide} direction={"prev"} />
</div>
</div>
<div className="Album_GridContainer">
{albums &&
albums.map((item, index) => {
return (
<AlbumImage
className="Album_gridImage"
key={index}
image={item}
/>
);
})}
</div>
</div>
);
}
export default Album;
THis is my AlbumContext :
import React, { createContext, useState } from "react";
export const AlbumContext = createContext();
export const AlbumContextProvider = ({ children }) => {
const [albums, setAlbums] = useState();
const [modal, setModal] = useState(false);
const [clickedImg, setClickedImg] = useState("");
const showImg = (img) => {
setClickedImg(img);
setModal(true);
console.log(clickedImg);
};
const getData = async () => {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/albums/1/photos"
);
const obj = await response.json();
console.log(obj);
setAlbums(obj);
} catch (error) {
// console.log(error.response.data.error);
console.log(error);
}
};
console.log(`Albums >>>`, albums);
return (
<AlbumContext.Provider
value={{ albums, getData, showImg, modal, setModal, clickedImg }}
>
{children}
</AlbumContext.Provider>
);
};
Thanks very much in advance
Your clickedImg starts out as the empty string:
const [clickedImg, setClickedImg] = useState("");
And in the consumer, you do:
const [slideIndex, setSlideIndex] = useState(clickedImg.id);
So, it takes the value of clickedImg.id on the first render - which is undefined, because strings don't have such properties. As a result, both before and after fetching, slideIndex is undefined, so after fetching:
albums ? albums[slideIndex].url : "no data"
will evaluate to
albums[undefined].url
But albums[undefined] doesn't exist, of course.
You need to figure out what slide index you want to be in state when the fetching finishes - perhaps start it at 0?
const [slideIndex, setSlideIndex] = useState(0);
maybe because your code for checking albums is empty or not is wrong and its always return true condition so change your code to this:
<div className="Album_GridContainer">
{albums.length > 0 &&
albums.map((item, index) => {
return (
<AlbumImage
className="Album_gridImage"
key={index}
image={item}
/>
);
})}
</div>
change albums to albums.length
import { useSelector, useDispatch } from "react-redux";
import { useEffect, useState } from "react";
import { useHistory } from "react-router-dom";
import { IoMdArrowRoundBack } from "react-icons/io";
import axios from "axios";
import { fetchMovies } from "../../feautures/movies/moviesSlice";
import Rating from "../../components/UI/Rating/Rating";
import request from "../../requests";
import "./SingleMoviePage.scss";
import SimilarMovies from "../../components/SimilarMovies/SimilarMovies";
const SingleMoviePage = ({ match }) => {
const dispatch = useDispatch();
const [movieDetails, setMovieDetails] = useState({});
const [movieCredits, setMovieCredits] = useState({});
const history = useHistory();
console.log("single rendered")
// number month to string
const date = new Date(movieDetails.release_date);
const dateWithMonthName =
date.getFullYear() +
"-" +
date.toLocaleString("en-EN", { month: "long" }) +
"-" +
date.getDay();
/* params */
const movieId = match.params.id;
const page = match.params.page;
const genre = match.params.genre;
/* movies reducer handle */
const moviesStatus = useSelector((state) => state.movies.status);
/* base urls */
const baseImgUrl = "https://image.tmdb.org/t/p/original";
const movieDetailUrl = `https://api.themoviedb.org/3/movie/${movieId}?api_key=c057c067b76238e7a64d3ba8de37076e&language=en-US`;
const movieCastUrl = `https://api.themoviedb.org/3/movie/${movieId}/credits?api_key=c057c067b76238e7a64d3ba8de37076e&language=en-US`;
// go home page
const goHOme = () => {
history.goBack()
};
// fetch movie cast
useEffect(() => {
const fetchMovieCast = async () => {
let response = await axios.get(movieCastUrl);
response = response.data;
setMovieCredits(response);
};
fetchMovieCast();
}, [movieCastUrl]);
// fetch movie details
useEffect(() => {
const fetchMovieDetails = async () => {
let response = await axios.get(movieDetailUrl);
response = response.data;
setMovieDetails(response);
};
fetchMovieDetails();
}, [movieDetailUrl]);
let content;
if (moviesStatus === "loading") {
} else if (moviesStatus === "succeeded") {
content = (
<div
className="single-movie__container"
style={{
backgroundImage: `url(${
movieDetails.backdrop_path
? baseImgUrl + movieDetails.backdrop_path
: baseImgUrl + movieDetails.poster_path
})`,
}}
>
<div className="single-movie__details">
<IoMdArrowRoundBack
className="single-movie__back"
onClick={goHOme}
size={65}
color={"#e50914"}
/>
<h1 className="single-movie__title">{movieDetails.title}</h1>
<div className="single-movie__rate">
<Rating
rating={movieDetails.vote_average}
className="single-movie__stars"
/>
</div>
<p className="single-movie__overview">{movieDetails.overview}</p>
<div className="single-movie__informations single-movie__informations--genres">
<label className="single-movie__informations-heading">Genres</label>
<div className="single-movie__informations-container">
{movieDetails.genres?.map((genre) => {
return <div className="single-movie__info">{genre.name}</div>;
})}
</div>
</div>
<div className="single-movie__informations single-movie__informations--stars">
<label className="single-movie__informations-heading">
Starring
</label>
<div className="single-movie__informations-container">
{movieCredits.cast?.slice(0, 4).map((star) => {
return <div className="single-movie__info">{star.name}</div>;
})}
</div>
</div>
<div className="single-movie__informations single-movie__informations--released">
<label className="single-movie__informations-heading">
Release Date
</label>
<div className="single-movie__informations-container">
<div className="single-movie__info">{dateWithMonthName}</div>
</div>
</div>
<div className="single-movie__informations single-movie__informations--production">
<label className="single-movie__informations-heading">
Production
</label>
<div className="single-movie__informations-container">
{movieDetails.production_countries?.slice(0, 2).map((country) => {
return <div className="single-movie__info">{country.name}</div>;
})}
</div>
</div>
</div>
<SimilarMovies movieId={movieId} />
</div>
);
}
useEffect(() => {
if (genre === "POPULAR") {
dispatch(fetchMovies(request.fetchPopular(page)));
} else if (genre === "NOW PLAYING") {
dispatch(fetchMovies(request.fetchNowPlaying(page)));
} else if (genre === "UP COMING") {
dispatch(fetchMovies(request.fetchUpComing(page)));
}
}, [dispatch, genre, page]);
return <div className="single-movie">{content}</div>;
};
export default SingleMoviePage;
Hi all.When i clicked Card compenent it navigate me to the SingleMoviePage component.But SingleMoviePage component re-render five times.How can i find this issues's source ? And how can i prevent that ? Finally is there any problem to fetch MovieCast and MovieDetails in same useEffect hook ?
github repo : https://github.com/UmutPalabiyik/hope-movie-app
demo : https://hope-movie.web.app/page/1
The first 2 useEffect hooks fetch data separately which then update your local states, which then triggers re-rendering.
If you don't want to re-render after each data fetch (state update), I'd suggest adding a loading state. Set it to true first and return a loading icon as your content. Then after both movieDetails and movieCredits have been populated, set it to false and return the actual content. This should render twice in total.
Have you considered graphql? GraphQL can combine your api calls into one and it also handles loading state and error state.
Whatever solution you have, re-rendering will happen when you are fetching data. Initial render won't have any data and after fetching data it must re-render.
You should use only one useEffect hook your code is running for all three. React will handle the rest itself.
I am trying to fetch images by their ids. The architecture of backend is as follows: DB stores images in binary and there is another table that stores images ids.
I am using apollo client on front end to prefetch images ids and then send another set of fetch requests.
Unfortunately I get Error: Too many re-renders. React limits the number of renders to prevent an infinite loop. Could anyone help me to
1) figure out why it happens. I see that there is bunch of pending promises in the stack.
and 2) how it can be refactored to better architecture.
import React, {useState} from 'react'
import {useQuery} from "#apollo/react-hooks";
import {gql} from 'apollo-boost';
const apiEndpoint = 'http://localhost:9211';
const getProductImage = function (id) {
return gql`
{
productById(id: "${id}") {
images {
imageId
}
}
}`
};
const fetchImage = (imageUrl, allImgsArr) => {
return fetch(imageUrl)
.then(res => res.blob())
.then(img => allImgsArr.push(URL.createObjectURL(img)))
};
const ItemPage = (props) => {
const [id] = useState(props.match.params.id);
const {data} = useQuery(getProductImage(id));
let imagesIds = [];
if (data) {
data.productById.images.forEach(image => {
imagesIds.push(image.imageId)
});
}
const [imagesUrls, setImagesUrl] = useState([]);
// MULTIPE FETCH RETRIEVALS START
for (let imId of imagesIds) {
setImagesUrl(imagesUrls => [...imagesUrls, fetchImage(`${apiEndpoint}/image/${imId}`, imagesUrls)]);
}
// MULTIPE FETCH RETRIEVALS END
return (
<>
<div>
<div>
<img src={imagesUrls[0] ? imagesUrls[0] : ''} alt="main item 1 photo"/>
</div>
<div>
<div>
<img src={imagesUrls[1] ? imagesUrls[1] : ''} alt="Additional item 1 photo"/>
</div>
</div>
</div>
</>
)
};
export default ItemPage;
your query should be a constant , not function.
const GET_PRODUCT_IMAGE = gql`
query getProduct($id:String!) {
productById(id: $id) {
images {
imageId
}
}
}
}`
// pass variables like this
const {data} = useQuery(GET_PRODUCT_IMAGE, { variables: { id },
});
More Info : https://www.apollographql.com/docs/react/data/queries/