Ionic w/ React Trigger Infinite Scroll until the page is filled - reactjs

I have the following component.
const Feed: React.FC<FeedProps> = memo((props) => {
// ... lines skipped for brevity
return (
<div>
<IonList>
{
data.map((item) => {
return <FeedItem key={item.id!} item={item} />
})
}
</IonList>
<IonInfiniteScroll onIonInfinite={(e: CustomEvent<void>) => __handleIonInfinite(e)}>
<IonInfiniteScrollContent loadingText="Loading items" />
</IonInfiniteScroll>
</div>
);
async function __handleIonInfinite(e: CustomEvent<void>) {
const result = await __get();
if (result && result.length < environment.PAGE_SIZE) {
(e.target as HTMLIonInfiniteScrollElement).disabled = true;
}
(e.target as HTMLIonInfiniteScrollElement).complete();
}
});
Everything works fine when scrolling. I have one question though. At first, the page is empty and I need to scroll to trigger the event. I was wondering if there is a way of triggering the scroll event such that it fills the page?
I was thinking of implementing this myself but maybe it comes out of the box? Besides, it tends to be a little complex because, depending on your screen size, I would need to see how many times I should trigger this.

Related

React + Api+ Context

I have a simple React app. On the 'home' page you can search movies from an API and add a movie to a list of favorited. I'm using Context to store which movies are on the list and pass it to the 'favorites' page where those items are rendered. It works well up to a point.
Once on the 'favorites' page, when I remove a movie, I would like the page to then show the updated elements. Instead, I have the elements I already had there plus the elements from the updated list.
So let's say my favorited movies were 'spiderman', 'batman' and 'dracula'. when I remove 'dracula' from the list, I suddenly have the cards of 'spiderman', 'batman, 'dracula', 'spiderman'(again) and 'batman'(again).
When I reload the 'favorites' page, it all works as intended. I just would like for it to be updated correctly upon removing the movie.
Any advice?
Here is the code for the Home page, Favorite page, DataContext and the Card component
import React, { createContext, useState, useEffect } from "react";
export const DataContext = createContext();
function DataContextProvider({ children }) {
const [favorited, setFavorited] = useState([]);
useEffect(() => {
const savedMovies = localStorage.getItem("movies");
if (savedMovies) {
setFavorited(JSON.parse(savedMovies));
}
}, []);
useEffect(() => {
localStorage.setItem("movies", JSON.stringify(favorited));
}, [favorited]);
function addToFavorites(id) {
setFavorited((prev) => [...prev, id]);
}
function removeFromFavorited(id) {
const filtered = favorited.filter(el => el != id)
setFavorited(filtered)
}
return (
<DataContext.Provider value={{ favorited, addToFavorites, removeFromFavorited}}>
{children}
</DataContext.Provider>
);
}
export default DataContextProvider;
function Favorites(props) {
const ctx = useContext(DataContext);
const [favoriteMovies, setFavoriteMovies] = useState([]);
useEffect(() => {
const key = process.env.REACT_APP_API_KEY;
const savedMovies = ctx.favorited;
for (let i = 0; i < savedMovies.length; i++) {
axios
.get(
`https://api.themoviedb.org/3/movie/${savedMovies[i]}?api_key=${key}&language=en-US`
)
.then((res) => {
setFavoriteMovies((prev) => [...prev, res.data]);
});
}
}, [ctx.favorited]);
return (
<>
<Navbar />
<main>
<div className="favorites-container">
{favoriteMovies.map((movie) => {
return <Card key={movie.id} movie={movie} />;
})}
</div>
</main>
</>
);
}
function Home(props) {
const [moviesData, setMoviesData] = useState([]);
const [numOfMovies, setNumOfMovies] = useState(10);
const [search, setSearch] = useState(getDayOfWeek());
const [spinner, setSpinner] = useState(true);
const [goodToBad, setGoodToBad] = useState(null);
function getDayOfWeek() {
const date = new Date().getDay();
let day = "";
switch (date) {
case 0:
day = "Sunday";
break;
case 1:
day = "Monday";
break;
case 2:
day = "Tuesday";
break;
case 3:
day = "Wednesday";
break;
case 4:
day = "Thursday";
break;
case 5:
day = "Friday";
break;
case 6:
day = "Saturday";
break;
}
return day;
}
function bestToWorst() {
setGoodToBad(true);
}
function worstToBest() {
setGoodToBad(false);
}
useEffect(() => {
const key = process.env.REACT_APP_API_KEY;
axios
.get(
`https://api.themoviedb.org/3/search/movie?api_key=${key}&query=${search}`
)
.then((res) => {
setMoviesData(res.data.results);
//console.log(res.data.results)
setSpinner(false);
setGoodToBad(null);
});
}, [search]);
return (
<>
<Navbar />
<main>
<form>
<input
type="text"
placeholder="Search here"
id="search-input"
onChange={(e) => {
setSearch(e.target.value);
setNumOfMovies(10);
}}
/>
{/* <input type="submit" value="Search" /> */}
</form>
<div className="sorting-btns">
<button id="top" onClick={bestToWorst}>
<BsArrowUp />
</button>
<button id="bottom" onClick={worstToBest}>
<BsArrowDown />
</button>
</div>
{spinner ? <Loader /> : ""}
<div>
<div className="results">
{!moviesData.length && <p>No results found</p>}
{moviesData
.slice(0, numOfMovies)
.sort((a,b) => {
if(goodToBad) {
return b.vote_average - a.vote_average
} else if (goodToBad === false){
return a.vote_average - b.vote_average
}
})
.map((movie) => (
<Card key={movie.id} movie={movie} />
))}
</div>
</div>
{numOfMovies < moviesData.length && (
<button className="more-btn" onClick={() => setNumOfMovies((prevNum) => prevNum + 6)}>
Show More
</button>
)}
</main>
</>
);
}
export default Home;
function Card(props) {
const ctx = useContext(DataContext);
return (
<div
className={
ctx.favorited.includes(props.movie.id)
? "favorited movie-card"
: "movie-card"
}
>
<div className="movie-img">
<img
alt="movie poster"
src={
props.movie.poster_path
? `https://image.tmdb.org/t/p/w200/${props.movie.poster_path}`
: "./generic-title.png"
}
/>
</div>
<h2>{props.movie.original_title}</h2>
<p>{props.movie.vote_average}/10</p>
<button
className="add-btn"
onClick={() => ctx.addToFavorites(props.movie.id)}
>
Add
</button>
<button
className="remove-btn"
onClick={() => ctx.removeFromFavorited(props.movie.id)}
>
Remove
</button>
</div>
);
}
export default Card;
As mentioned before a lot of things cold be improved (you might want to check some react tutorial beginners related to best practices).
Anyway the main issue your app seems to be your callback after you get the response from the API (so this part):
useEffect(() => {
const key = process.env.REACT_APP_API_KEY;
const savedMovies = ctx.favorited;
for (let i = 0; i < savedMovies.length; i++) {
axios
.get(
`https://api.themoviedb.org/3/movie/${savedMovies[i]}?api_key=${key}&language=en-US`
)
.then((res) => {
setFavoriteMovies((prev) => [...prev, res.data]);
});
}
here you are calling setFavoriteMovies((prev) => [...prev, res.data]); but you actually never reset your favoriteMovies list.
So in your example favoriteMovies is ['spiderman', 'batman', 'dracula']. Then the useEffect callback executes with the array unchanged.
So you are making the requests just for 'spiderman' and 'batman' but your favoriteMovies array is ['spiderman', 'batman', 'dracula'] when the callback is entered (and this is why you end up appending those values to the existing ones and in the end your favoriteMovies == ['spiderman', 'batman', 'dracula', 'spiderman', 'batman'] in your example).
How to fix?
Quick fix would that might be obvious would be to reset the favoriteMovies at the beggining of useEffect. But that would be a extremly bad ideea since setting the state many times is terrible for performance reasons (each setState callback triggers a re-render) as well as for redability. So please don't consider this.
What I would suggest though would be to get all the values in the useEffect callback, put all the new favorite movies data in a variable and at the end of the function change the state in one call with the full updated list.
There are multiple ways to achieve this (async await is the best imo), but trying to alter the code as little as possible something like this should also work:
useEffect(() => {
const key = process.env.REACT_APP_API_KEY;
const savedMovies = ctx.favorited;
const favoriteMoviesPromises = [];
for (let i = 0; i < savedMovies.length; i++) {
favoriteMoviesPromises.push(
axios
.get(`https://api.themoviedb.org/3/movie/${savedMovies[i]}?api_key=${key}&language=en-US`)
.then((res) => res.data)
);
}
Promise.all(favoriteMoviesPromises).then((newFavoriteMovies) =>
setFavoriteMovies(newFavoriteMovies)
);
});
Please note I wasn't able to test this code since I don't have an exact reproduction of the error (so it might need some small adjustments). This code sample is rather a direction for your problem :)
Edit regarding the comment:
Despite the state issue, I would really recommend working on code cleanliness, efficiency and readability.
Examples (I put a few examples in code snippets to avoid a really long comment):
1. `function getDayOfWeek`:
First thing is that you don't need the `day` variable and all the break statement.
You could just return the value on the spot (this would also stop the execution of the function).
So instead of
case 0:
day = "Sunday";
break;
you could have
case 0:
return "Sunday";
Going even further you don't need a switch case at all. You could just create an array
`const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', "Saturday"]`
and just return daysOfWeek[date].
This would result in shorter and easier to read code.
2. Also this code is not really consistent. For example you have
onChange={(e) => {
setSearch(e.target.value);
setNumOfMovies(10);
}}
but also `onClick={bestToWorst}` which is just `function bestToWorst() { setGoodToBad(true) }`.
If this is not reusable you could just use `onClick={() => setGoodToBad(true)}`.
But even if you really want to keep the bestToWorst callback for whatever reason you could at least write and inline function
(something like `const bestToWorst = () => setGoodToBad(true)` and use it the same).
Anyway... From thoose 2 cases (bestToWorst and `Search here` onChange function),
the second one make more sense to be defined outside.
3. The next part is really hard to read and maintain:
{!moviesData.length && <p>No results found</p>}
{moviesData
.slice(0, numOfMovies)
.sort((a,b) => {
if(goodToBad) {
return b.vote_average - a.vote_average
} else if (goodToBad === false){
return a.vote_average - b.vote_average
}
})
.map((movie) => (
<Card key={movie.id} movie={movie} />
))}
Also this code doesn't belong in html.
You should at least put the slice and sort parts in a function.
Going further `if(goodToBad)` and `else if (goodToBad === false)` are also not ideal.
It would be best to use a separate function an example would be something like:
const getFormattedMoviesData = () => {
let formattedMoviesData = moviesData.slice(0, numOfMovies)
if(!goodToBad && goodToBad !== false) return formattedMoviesData;
const getMoviesDifference = (m1, m2) => m1.vote_average - m2.vote_average
return formattedMoviesData.sort((a,b) => goodToBad ? getMoviesDIfference(b,a) : getMoviesDIfference(a,b)
4. DataContext name doesn't suggest anything.
I would propose something more meaningfull (especially for contexts) like `FavoriteMoviesContext`.
In this way people can get an ideea of what it represents when they come across it in the code.
Additionally the context only contains `favorited, addToFavorites, removeFromFavorited`.
So rather than using
`const ctx = useContext(DataContext);`
you could just use
`const {favorited, addToFavorites, removeFromFavorited} = useContext(DataContext);`
and get rid of the ctx variable in your code
Regarding the api:
If the search api returns all the movie data you need you can take it from there and use it in the favorites.
Alternatively it would be great to have an endpoint to return a list of multiple movies
(so send an array of id's in the request and receive all of them).
But this is only possible if the backend supports it.
But otherwise, since the api might contain hundreds of thousands or even millions, having them all stored on the frontside state would be an overkill
(you can in some cases have this type lists stored in a redux state or a react context and filter them on frontend side.
But it won't be efficient for such a big volume of data).
Small conclusion: ignoring the state part there aren't big issues in the code (and for a personal project or for learning might be decent). But if someone else has to work on in or you have to come back on this code after a month might become a nightmare. (especially since it seems like the codebase is not very small)
And people trying to understand your code might find it hard as well (including when you are posting it on stack overflow). I highlighted just a few, but it should point in the right direction, I hope.
First of all, you should review the way you manage the favorite movies and that of what you want to do with them in your app. If you need to make a page to display the list of favorites, I would rather save in localstorage the necessary information for the list (cover, title, year, id, etc) without having to save the whole movie object. This will prevent you from having to call the API for each movie which will be very bad in terms of performance on your application. Also, it will prevent you from having to create another state on the Favorites page so it will solve your problem automatically (I think your problem came from the duplicate state you have).

Next.js refresh values without reloading page in props list

probably I got a little bit lost on the following task. I have an admin page in my application where you can see all the posts from the plattform. I'm requesting the posts from an api and Im displaying them on the page as a list. I Inserted two buttons to enable/disable the post by calling a function which does tag the post to be enabled/disabled.
Everything works fine, but I want to change the state of the button without reloading the page. Im passing disable parameter threw the button tag. I don't know why its not working, if I console.log the values its already changed there. Is this a proper way to use useeffect? I tried to use it but I failed using it correct.
Somebody can help please?
Simplified Code ( I removed the enable function, since its nearly the same like disable)
export default function Feed(props) {
const [postStatus, setPostStatus] = useState(props.posts)
async function disablePost(id, e){
e.preventDefault();
const userInput = { postId: id }
try{
const res = await axios.post(`${baseurl}/api/auth/admin/disable-post`, userInput, {
})
var currentPostStatus = postStatus;
currentPostStatus.map((el) => {
if(el.id === id){
el.disabled = true;
}
console.log(el.id)
});
setPostStatus(currentPostStatus)
console.log(postStatus)
}catch(error){
console.log(error)
return
}
}
return (
<>
<HeaderAdmin></HeaderAdmin>
<div>
{/* {console.log(props.userCount)} */}
<p>Alle Posts</p>
{postStatus.map((post) =>
<React.Fragment key={post.id}>
<p>{post.id}</p>
<p>{post.email}</p>
<p>{post.desc}</p>
<p>{post.disabled == true ? 'Post deaktviert' : 'Post aktiviert'}</p>
<button disabled={ post.disabled } onClick={(e) => disablePost(post.id, e)}>Post deaktivieren</button>
<button disabled={ !post.disabled } onClick={(e) => enablePost(post.id, e)}>Post aktivieren</button>
</React.Fragment>
)}
</div>
</>
);
}
Your screen can't refresh to the new version after you clicked the disable or enable.
var currentPostStatus = postStatus;
currentPostStatus.map((el) => {
if(el.id === id){
el.disabled = true;
}
});
In your code, you are only changing the internal property of postStatus, but React only cares about the reference of the object. so you need to do
setPostStatus([...currentPostStatus])
The above line create a new array. I personally believe this is something React should improve in the future, because half of the question about React in stackoverflow is talking about this :)

Is there any alternative to the used method 'scroll top' when a link is clicked?

When a user clicks a link that directs the user to a new page, it generally put the user's view in the middle of the new page, at the same position as the original link. To prevent this, we can use the well-known method; scrolling up from window events.
I would like to know if there are any alternatives or tips that will prevent the user from seeing the scrolling up. Ideally, I would like the view to be at the top straight away like a new open page.
Thank you,
I found the following solution in my case to behave like a new page:
const ParentComponent: React.FC = () => {
const [isViewTopPage, setViewTopPage] = useState(false);
const scrollToTheTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
let result = window.scrollY === 0;
setViewTopPage(result);
};
useEffect(() => {
// If the user refresh the page
if (window.scrollY === 0) {
setViewTopPage(true);
}
// User coming from the link clicked
if (!isViewTopPage) {
window.addEventListener('scroll', scrollToTheTop);
}
return () => window.removeEventListener('scroll',
scrollToTheTop);
}, [isViewTopPage]);
return (
<section id="parent-component">
{/* Your conditional rendering *}
{isViewTopPage && (
<Header/>
<YourChildComponent/>
<Footer/>
)}
</section>
);
};
Note: as my child component was not too down from the top viewport, the scrolling up was very fast. It might not be the case for you if your child component is too down. You might have to implement an animation while scrolling up for the user experience.

TypeError: Cannot read property 'scrollIntoView' of null In React Table

const modal =({messages})=>{
const messagesEnd = React.useRef(null);
const scrollToBottom = () => {
messagesEnd.current.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
scrollToBottom();
}, []);
const messagesComp = (single_message)=>{
<TableRow>
<div>
<p>{single_message}</p>
</div>
</TableRow>
}
return (
<div>
<Table>
<TableBody>
{messages ? messages.map(item, index => {
if(messages.length === index+1){
return <div ref={messagesEnd}>messagesComp(item)</div>
}else{
messagesComp(item)
}
}
) : null}
</TableBody>
</Table>
</div>
);
}
I've made an application where if you click on a button a modal pops up and on that modal there are comments which are in form of table rows, as the comments increase the user has to scroll all the to down and I want it to scroll automatically as the modal component mounts. I've used material-ui.
I tried it by ref and also tried others code available here but still it's showing me this error.
Your issue is that your render has two distinct paths, but only one of them sets the element reference:
if(messages.length === index+1){
return <div ref={messagesEnd}>messagesComp(item)</div>
} else {
messagesComp(item)
}
There is even a third path where messages is empty and you would also not get a reference set.
Without the reference, when useEffect is called (which will occur regardless of which way you rendered it), it will not have a reference stored in messagesEnd.current.
To deal with this, you need to either always have the element reference in your render, which does not seem to be the effect you want, or to check validity before using the ref:
const scrollToBottom = () => {
if (messagesEnd && messagesEnd.current) {
messagesEnd.current.scrollIntoView({ behavior: "smooth" });
}
};

onClick event not called when clicking

React onClick event not working when clicking on glyphicon.
const SortableItem = SortableElement(({value, parentId}) =>
<ListGroupItem bsStyle='success' style={{width:'300px',textAlign:'left'}}>
{value}
{parentId === null && <span className='glyphicon glyphicon-remove' style={{float:'right',width:'10px',height:'10px'}}
onClick={e => {e.preventDefault(); console.log('yes')}}/>}
</ListGroupItem>
);
I ran into something similar. My onClick events on my <a> elements were not getting triggered when a user clicked them.
This is what I was doing wrong and maybe you are doing the same mistake as what I was doing. Without more context, it's impossible to diagnose what your actual problem is.
(code is basically saying, when I click the background, stop propagation of the click)
// Example of what I was doing WRONG
const Dialog = ({ onClose, children }) => {
const $overlay = React.useRef(null)
// on mount
React.useEffect(() => {
const handleBackgroundClick = (e) => {
e.stopPropagation() // <<-- This was the line causing the issue
onClose()
})
$overlay.current?.addEventListener('click', handleBackgroundClick)
// on unmount
return () => {
$overlay.current?.removeEventListener('click', handleBackgroundClick)
}
}, [])
return (
<div className="Dialog" ref={$overlay}>{children}</div>
)
}
Basically what I'm saying is this:
Do not use event.stopPropagation() directly on DOM elements in React
The events need to be able to bubble all the way to the top level or in-built React event's will stop working.
I ended up getting around the issue by adding an extra div inside the dialog to act as the overlay.
// How I fixed the issue
const Dialog = ({ onClose, children }) => {
return (
<div className="Dialog">
<div className="Dialog__overlay" onClick={()=> onClose()}></div>
<div className="Dialog__content">{children}</div>
</div>
)
}
Note: These are simplified examples to demonstrate a point. The exact code used was more complex. Using this code as displayed here would cause accessibility issues on your website.

Resources