I have a component that performs a pagination feature with blog posts. Each time the "load more" button is clicked, five more posts are loaded to the bottom of the page. I am drawing a blank right now on how to solve an issue with it though.
The Problem:
Once the "load more" button is clicked and all of the available posts are visible, the "load more" button should go away.
The Code:
const Posts = ({ state }) => {
const [categories, setCategories] = useState([]);
const [categoryId, setCategoryId] = useState();
const [page, setPage] = useState(1);
const [posts, setPosts] = useState([]); // Posts for each category
const [allPosts, setAllPosts] = useState([]); // All posts from all categories
// Get all of the available categories
useEffect(() => {
fetch(state.source.api + "/wp/v2/categories")
.then(response => response.json())
.then(data => {
setCategories(data);
})
}, []);
useEffect(() => {
if (categoryId) {
fetch(state.source.api + "/wp/v2/posts?categories=" + categoryId + "&per_page=5")
.then((response) => response.json())
.then((data) => {
setPosts(data);
});
}
}, [categoryId]);
// Get posts for each category
useEffect(() => {
if (!categoryId) {
return;
}
let url = state.source.api + "/wp/v2/posts?categories=" + categoryId + "&per_page=5";
if (page > 1) {
url += `&page=${page}`;
}
fetch(url)
.then((response) => response.json())
.then((data) => {
setPosts([...posts, ...data]);
});
}, [categoryId, page]);
useEffect(() => {
let url = state.source.api + "/wp/v2/posts?per_page=5";
if (page > 1) {
url += `&page=${page}`;
}
fetch(url)
.then((response) => response.json())
.then((data) => {
setAllPosts([...allPosts, ...data]);
});
}, [page]);
// Add View All button to category buttons array
const allCategories = categories.map((category, i) => (category))
allCategories.push("View All");
return (
<>
{allCategories.map((category, i) => {
return (
(category === "View All") ? (
<button onClick={() => { setPosts([]) }}>View All</button>
) : (
<button className="btn" key={i} onClick={() => {
setPage(1);
setPosts([]);
setCategoryId(category.id);
}}>{category.name}</button>
)
)
})}
<div>
{posts.length === 0 ? (
<>
{allPosts.map((allPost, i) => {
return (
<li key={i}>{allPost.title.rendered}</li>
)
})}
<button onClick={() => { setPage(page + 1); }}>Load more</button>
</>
) : (
<>
<ol>
{posts.map((post, i) => {
return (
<li key={i}>{post.title.rendered}</li>
)
})}
</ol>
<button onClick={() => { setPage(page + 1); }}>Load more</button>
</>
)}
</div>
</>
)
}
Say there are a maximum of 3 pages worth of posts, five being viewed at a time - so 15 posts total... once all 3 pages are visible, and I click "load more" again (even though no more posts are available), I get a console error that ... my api... GET /wp/v2/posts?categories=78&per_page=5&page=4 400 (Bad Request), which obviously means that there was no page 4 to fetch because there are no more posts.
I think this requires an if statement, but I am not sure how I would get started writing it. Does anyone have any input?
You should have a variable called totalPages, which hold the number of pages, and then compare between totalPages and page states, something like that:
{page <= totalPages && <button onClick={() => { setPage(page + 1); }}>Load more</button>}
in this case if page is 3 and totalPages is 4 the button will not display
Try an npm package called "react-paginate", with this you can easily implement the pagination logic without messing up with the code, and here is the link for this package https://www.npmjs.com/package/react-paginate
Just go with the documentation and you can figure out how to do this, but if you face any problem then don't hesitate, just ask me.
Related
I am fetching data in React, I am fetching users, so I have a state (I am using redux) allUsers, in which I am storing fetched users and adding more users with infinite scroll once scrollbar reaches the bottom. The problem is that when new data is being added to the allUsers state, React is re-rendering all of the users because I am using .map to render them. Could you please tell me how to fix that? Here is the code:
const allUsers = useSelector(state => state.setAllUsersReducer);
const page = useSelector(state => state.setPageReducer);
const loading = useSelector(state => state.setLoadingReducer);
const dispatch = useDispatch();
const fetchAllUsers = () => {
console.log(page)
fetch(`${url}/${page}/15`)
.then(res => res.json())
.then(data => {
dispatch(setAllUsers(data.list))
})
.catch(err => console.log('Error message: ', err))
}
console.log(allUsers)
useEffect(() => {
fetchAllUsers();
}, [page])
const handleScroll = () => {
dispatch(setPage());
}
window.onscroll = function () {
if(window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight) {
handleScroll();
}
}
return (
<div className="allUsersList">
{
allUsers.length > 0 ? (
allUsers.map((user, index) => (
<User key={index + 1} name={user.name} lastName={user.lastName} prefix={user.prefix} title={user.title} img={user.imageUrl}/>
))
) : (
<div> Loading... </div>
)
}
</div>
)
}
The idea is to fetch cards that user used in the database and compare with the all cards that are listen on screen. Basically user is not supposed to see cards which he already used.
const loadSaveInvestments = async (
_req: express.Request,
res: express.Response
) => {
const findSaveInvestments = await prisma.investmentSave.findMany({
select: {
investment_id: true,
},
});
if (!findSaveInvestments) throw new HttpException(400, SAVE_INVEST_MISSING);
res.send(findSaveInvestments);
};
So, I'm sending all of the saved investment cards. Now, in the React - I'm fetching the sent data from the service (all cards and saved cards)
const fetchUserSaveInvestments = async () => {
const res = await axios.get(`${baseURL}/investments-save`);
return res;
};
const [cards, setCards] = useState([]);
const [saveCards, setSaveCards] = useState([]);
useEffect(() => {
setLoading(true);
investService.fetchUserSaveInvestments().then((res) => {
setSaveCards(res.data);
});
investService.fetchCards().then((res) => {
setCards(res.data.investments);
setLoading(false);
});
Now - the main part. I want to map card only if it's id is not in the save cards state, so I've tried something like that.. I've marked the point where I got kinda stuck, I mean, am I supposed to create something like double map or what? Idk if I'm going in the right direction.
<div className={classes.CardsContainer}>
<div className={classes.CardsSelector}>
{loading && <p> its loading </p>}
{!loading && (
<>
{cards
.filter(
(card) => card.id !== saveCards.?????.investments_id
)
.map((card) => (
<Card
{...card}
handleClick={() => handleChooseCard(card)}
/>
))}
</>
)}
</div>
Thanks in advance.
I'd try using a Set to keep track of the saved card IDs since it provides O(1) time complexity lookups (fast, better than searching another array for each filter candidate).
const [loading, setLoading] = useState(false)
const [cards, setCards] = useState([])
const [saveCards, setSaveCards] = useState(new Set())
useEffect(async () => {
setLoading(true);
const { data: savedCards } = await investService.fetchUserSaveInvestments()
const savedCardIds = savedCards.map(({ investments_id }) => investments_id)
setSaveCards(new Set(savedCardIds))
const { data: { investments: cards } } = await investService.fetchCards()
setCards(cards)
setLoading(false)
}, [])
You can then use a memo to do the filtering
const myCards = useMemo(() => cards.filter(({ id }) => !saveCards.has(id)), [cards, saveCards])
{myCards.map(card => (
<Card
{...card}
handleClick={() => handleChooseCard(card)}
/>
))}
Simply declare the following state:
const [investmentsIds, setInvestmentIds] = useState([]);
Add few more lines of code here:
investService.fetchUserSaveInvestments().then((res) => {
setSaveCards(res.data);
let savedCardIds = [];
/* push investment_id in an array */
res.data.forEach((value)=>{
savedCardIds.push(value.investments_id);
})
setInvestmentIds(savedCardIds);
});
Your JSX:
<div className={classes.CardsContainer}>
<div className={classes.CardsSelector}>
{loading && <p> its loading </p>}
{!loading && (
{cards.map((card) => (
{investmentsIds.includes(card.id) &&
<Card {...card}
handleClick={() => handleChooseCard(card)}/>
}
))}
)}
</div>
</div>
I'm getting data from Django Rest API and React for Frontend, and I need to create the pagination with posts. I did it all in pagination component. I created the state with current page and I'm changing it by clicking on the page button in component like this:
const Paginator = () => {
const [count, setCount] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(null);
const [nextPage, setNextPage] = useState(null);
const [previousPage, setPreviousPage] = useState(null);
const [valid, setValid] = useState(false);
useEffect(() => {
fetch(`http://127.0.0.1:8000/api/software/?p=${currentPage}`)
.then(response => response.json())
.then(data => {
setCount(data.count);
setTotalPages(data.total_pages)
setNextPage(data.links.next);
setPreviousPage(data.links.previous);
setValid(true);
})
}, [currentPage]);
...
return (
<>
{
...
<PbStart style={style} totalPages={range(1, totalPages+1)} setCurrentPage={setCurrentPage} />
...
}
</>
);
};
const PagItem = ({key, handleClick, className, title, name }) => {
return (
<li key={key} onClick={handleClick}>
<Link to='/' className={className} title={`Go to page ${title}`}>
{name}
</Link>
</li>
);
};
const PbStart = ({ style, totalPages, setCurrentPage }) => {
return (
...
{totalPages.map(p => (
<PagItem key={p} handleClick={() => setCurrentPage(p)} title={p} name={p} />
))}
...
);
};
And in posts component I don't know how to change current page, or getting it from the paginaton component. I've written that like this:
const Softwares = () => {
const [softwares, setSoftwares] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [valid, setValid] = useState(false);
useEffect(() => {
fetch(`http://127.0.0.1:8000/api/software/?p=${currentPage}`)
.then(response => response.json())
.then(data => {
setSoftwares(data.results);
setValid(true);
})
}, [currentPage]);
return (
<>
{
...
{softwares.map(s => (
<Article key={s.id} pathname={s.id} title={s.title} image={s.image} pubdate={s.pub_date} icon={s.category.parent.img} categoryID={s.category.id} categoryName={s.category.name} dCount={s.counter} content={s.content} />
))}
...
}
</>
);
};
So, how to do that(get the current page from pagination component or another way)?
I think a Paginator's job is only moving between pages and updating current page state. It should not be fetching data by itself, you can provide functionality to do extra work with props.
I haven't tested this, but this might be a good starting point.
With the example below you'll have a list of articles and then below it next and previous buttons.
In Softwares, as you can see I am passing the same function for handling next and previous pages, you can refactor it to have one function like onPageMove and call this function handleNext and handlePrev.
I added two separate functions if you have want to handle something different in either.
const Paginator = ({
total, // Required: Total records
startPage = 1, // Start from page / initialize current page to
limit = 30, // items per page
onMoveNext = null, // function to call next page,
onMovePrev = null, // function to call previous page
}) => {
const [currentPage, setCurrentPage] = useState(startPage);
const canGoNext = total >= limit;
const canGoPrev = currentPage > 1;
function handleNext(e) {
if (canGoNext) {
setCurrentPage((prevState) => prevState+1);
onMoveNext && onMoveNext({ currentPage });
}
}
function handlePrev(e) {
if (canGoPrev) {
setCurrentPage((prevState) => prevState-1);
onMovePrev && onMovePrev({ currentPage });
}
}
return (
<div>
<button onClick={handlePrev} disabled={!canGoPrev}>Prev</button>
<button onClick={handleNext} disabled={!canGoNext}>Next</button>
</div>
);
};
Here is how you can use Paginator in other components.
const PER_PAGE = 30; // get max # of records per page
const Softwares = () => {
const [softwares, setSoftwares] = useState([]);
const [valid, setValid] = useState(false);
const onFetchData = ({ currentPage }) => {
fetch(`http://127.0.0.1:8000/api/software/?p=${currentPage}&per_page=${PER_PAGE}`)
.then(response => response.json())
.then(data => {
setSoftwares(data.results);
setValid(true);
})
}
useEffect(() => {
onFetchData({ currentPage: 1 })
}, []);
return (
<>
{softwares.map(s => (
<Article key={s.id} pathname={s.id} title={s.title} image={s.image} pubdate={s.pub_date} icon={s.category.parent.img} categoryID={s.category.id} categoryName={s.category.name} dCount={s.counter} content={s.content} />
))}
<Paginator total={softwares.length} limit={PER_PAGE} onMoveNext={onFetchData} onMovePrev={onFetchData} />
</>
);
};
I have a component in which I am trying to add pagination functionality. I have made good progress in the structure so far. I have category buttons, where when a category button is selected, the element below shows a list of posts within that category.
For the pagination functionality, I using the WordPress REST API to pull in 5 posts at a time for each category. I am trying to create a "load more" button after the 5th post, where onClick it will load the next 5 posts:
const Posts = ({ state, actions }) => {
const [categories, setCategories] = useState([]);
const [categoryId, setCategoryId] = useState();
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch(state.source.api + "/wp/v2/categories")
.then(response => response.json())
.then(data => {
setCategories(data);
})
}, []);
useEffect(() => {
if (categoryId) {
fetch(state.source.api + "/wp/v2/posts?categories=" + categoryId + "&per_page=5")
.then((response) => response.json())
.then((data) => {
setPosts(data);
});
}
}, [categoryId]);
const [morePosts, setMorePosts] = useState([]);
useEffect(() => {
if (categoryId) {
fetch(state.source.api + "/wp/v2/posts?categories=" + categoryId + "&per_page=5&page=" + 2)
.then((response) => response.json())
.then((data) => {
setMorePosts(data);
});
}
}, [categoryId]);
return (
<>
{categories.length > 0 ? (
categories.map((category, i) => {
return (
<button key={i} onClick={() => setCategoryId(category.id)}>{category.name}</button>
)
})
) : (
<p>Loading...</p>
)
}
<div>
{posts.length === 0 ? (
<p>No posts...</p>
) : (
<>
<ol>
{posts.map((post, i) => {
return (
<li key={i}>{post.title.rendered}</li>
)
})}
</ol>
<button onClick={() => setMorePosts(category.id)}>Load More</button>
{console.log(morePosts.map((post) => post.title.rendered))}
</>
)}
</div>
</>
)
}
As you can see, under the last button I am consoling the array, and it does return the next 5 posts for the selected category. I am stuck on how to turn that array from the console into actually being shown on the page.
I would do it this way:
Load more button changes the page number.
When page number is changed: useEffect fires and posts are refetched/rerendered based on page number param.
const [page, setPage] = useState(1);
const [posts, setPosts] = useState([]);
useEffect(() => {
if (!categoryId) {
return;
}
let url = `${state.source.api}/wp/v2/postscategories=${categoryId}&per_page=5"`;
if (page > 1) {
url += `&page=${page}`;
}
fetch(url)
.then((response) => response.json())
.then((data) => {
setPosts([...posts, ...data]);
});
}, [categoryId, page]);
<button onClick={() => { setPage(page + 1); }}>Load more</button>
I have a React component that renders a list of items that are being mapped over, and shows the id for each item. When the component first loads, the list items appear, but then a second or two later disappears and in the console returns undefined.
The component being rendered is:
const Posts = ({ state }) => {
const [categories, setCategories] = useState([]);
const [categoryId, setCategoryId] = useState();
const [page, setPage] = useState(1);
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch(state.source.api + "/wp/v2/categories")
.then(response => response.json())
.then(data => {
setCategories(data);
})
}, []);
useEffect(() => {
if (categoryId) {
fetch(state.source.api + "/wp/v2/posts?categories=" + categoryId + "&per_page=5")
.then((response) => response.json())
.then((data) => {
setPosts(data);
});
}
}, [categoryId]);
useEffect(() => {
if (!categoryId) {
return;
}
let url = state.source.api + "/wp/v2/posts?categories=" + categoryId + "&per_page=5";
if (page > 1) {
url += `&page=${page}`;
}
fetch(url)
.then((response) => response.json())
.then((data) => {
setPosts([...posts, data]);
});
}, [categoryId, page]);
return (
<>
{categories.length > 0 ? (
categories.map((category, i) => {
return (
<button key={i} onClick={() => setCategoryId(category.id)}>{category.name}</button>
)
})
) : (
<p>Loading...</p>
)
}
<div>
{posts.length === 0 ? (
<p>No posts...</p>
) : (
<>
<ol>
{posts.map((post, i) => {
console.log(post.id);
return (
<li key={i}>{post.id}</li>
)
})}
</ol>
<button onClick={() => { setPage(page + 1); }}>Load more</button>
</>
)}
</div>
</>
)
}
And the console shows:
I have a lot of code commented out above this component, so the line in the console is referring to console.log(post.id);.
Does anyone have an idea of what could be causing this?
This useEffect is not needed. Both useEffects on initial load trying to hit same endpoint with same url params.
useEffect(() => {
if (categoryId) {
fetch(state.source.api + "/wp/v2/posts?categories=" + categoryId + "&per_page=5")
.then((response) => response.json())
.then((data) => {
setPosts(data);
});
}
}, [categoryId]);
And I think arrays are not merged correctly here:
try to replace:
setPosts([...posts, data]);
to:
setPosts([...posts, ...data]);