So I'm building my portfolio in next.js, and in order to avoid showing partly loaded gallery images, I wanted to use a 'loading' state, which will turn off (become 0) once all the photos are loaded perfectly. It works just fine unless I refresh the page. The loading doesn't become 0 (false) when I refresh the page. When I change the category and come back, it again starts working fine. Only stops on the page whenever I refresh the page. What can be the problem?
P.S. When 'loading' is 1 (true) the opacity of the grid is 0.
Thanks a lot in advance!
import { GalleryLayoutStyle } from '../../style/componentStyles/GalleryLayoutStyle'
import { useContext, useEffect, useState } from 'react'
import Masonry from 'react-masonry-css'
const Gallery = ({ data }) => {
const [loading, setLoading] = useState(1)
const [count, setCount] = useState(0)
const allImagesCount = data.length
const increaseCount = () => {
setCount((prev) => prev + 1)
}
useEffect(() => {
setLoading(1)
}, [])
useEffect(() => {
if (count == allImagesCount) {
setLoading(0)
}
}, [count])
return (
<GalleryLayoutStyle loading={loading}>
{loading == 1 ? <h1>Loading</h1> : null}
<div className="grid">
<Masonry className="my-masonry-grid" columnClassName="my-masonry-grid_column">
{data.map((item) => (
<div className="gallery-box" key={item.id}>
<img src={item.url} onLoad={increaseCount} />
<div className="overlay">
<div className="title">
<h4>{item.title}</h4>
</div>
<div className="zoom">
<img src="/icons/loupe.svg" alt="" />
</div>
</div>
</div>
))}
</Masonry>
</div>
</GalleryLayoutStyle>
)
}
export default Gallery
It looks (nearly) all fine to me.
I would change only one little thing. Don't set loading initially to 1, but 0. Make it depending only on count.
Maybe give it a try with this:
const Gallery = ({ data }) => {
const [loading, setLoading] = useState(0)
const [count, setCount] = useState(0)
const allImagesCount = data.length
const increaseCount = () => {
setCount((prev) => prev + 1)
}
useEffect(() => {
if (count === allImagesCount) {
setLoading(0)
}else{
setLoading(1)
}
}, [count])
....
Related
I don't understand why my page can't recognize other pages when I click (for example on page 2, the same page appears again and again)
This is in MealNew.js component:
import React, {useEffect, useState } from "react";
import './MealNew.css';
import Card from "../UI/Card";
import AppPagination from "./AppPagination";
const MealNew = () => {
const [data, setData] = useState([]);
const [showData, setShowData] = useState(false);
const [query,setQuery] = useState('');
const[page,setPage] = useState(9);
const[numberOfPages,setNumberOfPages]= useState(10);
const handleClick = () => {
setShowData(true);
const link = `https://api.spoonacular.com/recipes/complexSearch?query=${query}&apiKey=991fbfc719c743a5896bebbd98dfe996&page=${page}`;
fetch (link)
.then ((response)=> response.json())
.then ((data) => {
setData(data.results)
setNumberOfPages(data.total_pages)
const elementFood = data?.map((meal,key) => {
return (<div key={key}>
<h1>{meal.title}</h1>
<img src={meal.image}
alt='e-meal'/>
</div> )
})
const handleSubmit = (e) => {
e.preventDefault();
handleClick();
}
useEffect(()=> {
handleClick();
},[page])
return (
<Card className="meal">
<form onSubmit={handleSubmit}>
<input
className="search"
placeholder="Search..."
value={query}
onChange={(e)=>setQuery(e.target.value)}/>
<input type='submit' value='Search'/>
</form>
<li className="meal">
<div className = 'meal-text'>
<h5>{showData && elementFood}</h5>
<AppPagination
setPage={setPage}
pageNumber={numberOfPages}
/>
</div>
</li>
</Card>
) }
export default MealNew;
This is in AppPagination.js component:
import React from "react";
import { Pagination } from "#mui/material";
const AppPagination = ({setPage,pageNumber}) => {
const handleChange = (page)=> {
setPage(page)
window.scroll(0,0)
console.log (page)
}
return (
<div >
<div >
<Pagination
onChange={(e)=>handleChange(e.target.textContent)}
variant="outlined"
count={pageNumber}/>
</div>
</div>
)
}
export default AppPagination;
Thanks in advance, I would appreciate it a lot
The only error I am getting in Console is this:
Line 64:3: React Hook useEffect has a missing dependency: 'handleClick'. Either include it or remove the dependency array react-hooks/exhaustive-deps
You are not following the spoonacular api.
Your link looks like this:
https://api.spoonacular.com/recipes/complexSearch?query=${query}&apiKey=<API_KEY>&page=${page}
I checked the spoonacular Search Recipes Api and there's no page parameter you can pass. You have to used number instead of page.
When you receive response from the api, it returns the following keys: offset, number, results and totalResults.
You are storing totalResults as totalNumberOfPages in state which is wrong. MUI Pagination count takes total number of pages not the total number of records. You can calculate the total number of pages by:
Math.ceil(totalRecords / recordsPerPage). Let say you want to display 10 records per page and you have total 105 records.
Total No. of Pages = Math.ceil(105/10)= 11
Also i pass page as prop to AppPagination component to make it as controlled component.
Follow the documentation:
Search Recipes
Pagination API
Complete Code
import { useEffect, useState } from "react";
import { Card, Pagination } from "#mui/material";
const RECORDS_PER_PAGE = 10;
const MealNew = () => {
const [data, setData] = useState([]);
const [showData, setShowData] = useState(false);
const [query, setQuery] = useState("");
const [page, setPage] = useState(1);
const [numberOfPages, setNumberOfPages] = useState();
const handleClick = () => {
setShowData(true);
const link = `https://api.spoonacular.com/recipes/complexSearch?query=${query}&apiKey=<API_KEY>&number=${page}`;
fetch(link)
.then((response) => response.json())
.then((data) => {
setData(data.results);
const totalPages = Math.ceil(data.totalResults / RECORDS_PER_PAGE);
setNumberOfPages(totalPages);
});
};
const elementFood = data?.map((meal, key) => {
return (
<div key={key}>
<h1>{meal.title}</h1>
<img src={meal.image} alt='e-meal' />
</div>
);
});
const handleSubmit = (e) => {
e.preventDefault();
handleClick();
};
useEffect(() => {
handleClick();
console.log("first");
}, [page]);
return (
<Card className='meal'>
<form onSubmit={handleSubmit}>
<input className='search' placeholder='Search...' value={query} onChange={(e) => setQuery(e.target.value)} />
<input type='submit' value='Search' />
</form>
<li className='meal'>
<div className='meal-text'>
<h5>{showData && elementFood}</h5>
<AppPagination setPage={setPage} pageNumber={numberOfPages} page={page} />
</div>
</li>
</Card>
);
};
const AppPagination = ({ setPage, pageNumber, page }) => {
const handleChange = (page) => {
setPage(page);
window.scroll(0, 0);
console.log(page);
};
console.log("numberOfPages", pageNumber);
return (
<div>
<div>
<Pagination
page={page}
onChange={(e) => handleChange(e.target.textContent)}
variant='outlined'
count={pageNumber}
/>
</div>
</div>
);
};
export default MealNew;
I am working on a react app that shortens a URL after a button is clicked. I am having a hard time figuring out the best logic to do so. The loading message does not show then when I click the button. The loading button appears and then disappears when the message loads.
The current behavior with the code below is the following.
When it renders "Loading..." does not show.
I click my "Shorten it" button and "Loading..." shows.
The shortened url appears below the "Loading..." button.
Shorten.js
import { useEffect, useState } from "react";
const Shorten = () => {
const [shortLink, setShortLink] = useState(null);
const [isPending, setIsPending] = useState(false);
const [input, setInput] = useState('example.org/very/long/link.html');
const url = "https://api.shrtco.de/v2/shorten?url=";
const fullUrl = (url.concat(input));
console.log(fullUrl);
useEffect(() => {
fetch(fullUrl)
.then(res => {
return res.json();
})
.then(data => {
setShortLink(data.result.short_link);
// setIsPending(false);
})
}, [fullUrl ])
// input
// value={input}
const loadMsg = () =>{
setIsPending(true);
}
return (
<main>
<section className="purple-card">
<input onInput={e => setInput(e.target.value)} type="text" placeholder="Shorten a link here..." className="shorten-input"/>
<button className="shorten-it" onClick={() => loadMsg()}>Shorten It!</button>
</section>
<section className="white-card">
{isPending && <div className="loading-text">Loading...</div>}
{shortLink && <div className="shorten-text">{shortLink}</div>}
<hr></hr>
<button className="shorten-it" >Copy</button>
</section>
</main>
);
}
export default Shorten;
There seem to be a few issues here. The reason the loading message and the shortened URL display at the same time is that their render conditions are not mutually exclusive. Fixing that is as simple as not showing the shortened URL while the component is loading.
Also something that can cause issues is that the "Shorten It!" button does not control actually performing the shortening action, it just sets the loading (pending) state to true. The shortening action runs whenever the input's value changes. Basically the loading state and the shortening action are fairly independent of each other.
To fix this you should only run the shortening action when the user clicks the button and at that point set the isPending state to true at the same time (then set back to false when done).
Instead of useEffect, you can just use a function that gets called when the button is clicked.
For example:
import { useState, useCallback } from "react";
const Shorten = () => {
const [shortLink, setShortLink] = useState(null);
const [isPending, setIsPending] = useState(false);
const [input, setInput] = useState('example.org/very/long/link.html');
const shortenUrl = useCallback(() => {
setIsPending(true);
const baseUrl = "https://api.shrtco.de/v2/shorten?url=";
const fullUrl = baseUrl + input;
fetch(fullUrl)
.then(res => res.json())
.then(data => {
setShortLink(data.result.short_link)
})
.finally(() => setIsPending(false));
}, [input]);
return (
<main>
<section className="purple-card">
<input onInput={e => setInput(e.target.value)} type="text" placeholder="Shorten a link here..." className="shorten-input"/>
<button className="shorten-it" onClick={shortenUrl}>Shorten It!</button>
</section>
<section className="white-card">
{isPending && <div className="loading-text">Loading...</div>}
{!isPending && shortLink && <div className="shorten-text">{shortLink}</div>}
<hr></hr>
<button className="shorten-it" >Copy</button>
</section>
</main>
);
}
export default Shorten;
Alternatively, to make the loading/result more clearing mutually exclusive, you can define a single variable with a value of either the loading message, the result URL, or nothing. For example:
const result = useMemo(() => {
if (isPending) {
return <div className="loading-text">Loading...</div>;
}
if (shortLink) {
return <div className="shorten-text">{shortLink}</div>;
}
return null;
}, [isPending, shortLink]);
Then render like so:
<section className="white-card">
{result}
</section>
In my Home component(I call it Home Page!) I am using Cards.JS component which has posts attribute as shown in following code.
const Home = () => {
const dispatch = useDispatch()
const isLoading = useSelector(state => state.isLoading)
const currentPage = useSelector((state) => state.idFor.currentPageHome)
const homePosts = useSelector((state) => state.posts)
useEffect(() => {
dispatch(setIsLoading(true))
dispatch(getAllPosts(currentPage))
}, [dispatch, currentPage])
return (
isLoading ? (
<Loader type="ThreeDots" color="#000000" height={500} width={80} />
) : (
<Cards posts={homePosts} setCurrentPage={setCurrentPageHome} currentPage={currentPage} pageName={"LATEST"} />
)
)
}
And Cards.Js is as following
const Cards = ({ posts, setCurrentPage, currentPage, pageName }) => {
console.log('Cards.JS called', posts);
const dispatch = useDispatch()
useEffect(() => {
dispatch(setIsLoading(false))
})
const handleNextPage = () => {
dispatch(setIsLoading(true))
dispatch(setCurrentPage(currentPage + 1))
}
const handlePreviousPage = () => {
dispatch(setIsLoading(true))
dispatch(setCurrentPage(currentPage - 1))
}
return (
<div className="container">
<h4 className="page-heading">{pageName}</h4>
<div className="card-container">
{
posts.map(post => <Card key={post._id} post={post} />)
}
</div>
<div className="page-div">
{currentPage !== 1 ? <span className="previous-page" onClick={handlePreviousPage}><</span>
: null}
<span className="next-page" onClick={handleNextPage}>></span>
</div>
</div>
)
}
My Problem:
When i come back to home page useEffect is called everytime and request same data to back-end which are already avaliable in Redux store.
Thanks in Advance :)
useEffect will run every time the component rerenders.
However, useEffect also takes a second parameter: an array of variables to monitor. And it will only run the callback if any variable changes in that array.
If you pass an empty array, it will only run once initially, and never again no matter how many times your component rerenders.
useEffect(() => {
dispatch(setIsLoading(false))
}, [])
I was implementing an Autocomplete feature while learning REACT by watching this Youtube Tutorial and here is the github repo of this project (incase you need to clone and run). I implemented it and it worked as expected. But there is a small functionality that it doesn't provide, i.e., I can't scroll down the Autocomplete list. Refer to this image for output. When I click the Side Scroll Bar The list Vanishes. How to activate that? Do I need to make another useEffect for Scroll Bar too?
Here is the App.js Code (Same as Github though)
import React, { useEffect, useState, useRef } from "react";
import logo from "./logo.svg";
import "./App.css";
const Auto = () => {
const [display, setDisplay] = useState(false);
const [options, setOptions] = useState([]);
const [search, setSearch] = useState("");
const wrapperRef = useRef(null);
useEffect(() => {
const pokemon = [];
const promises = new Array(20)
.fill()
.map((v, i) => fetch(`https://pokeapi.co/api/v2/pokemon-form/${i + 1}`));
Promise.all(promises).then(pokemonArr => {
return pokemonArr.map(value =>
value
.json()
.then(({ name, sprites: { front_default: sprite } }) =>
pokemon.push({ name, sprite })
)
);
});
setOptions(pokemon);
}, []);
useEffect(() => {
window.addEventListener("mousedown", handleClickOutside);
return () => {
window.removeEventListener("mousedown", handleClickOutside);
};
});
const handleClickOutside = event => {
const { current: wrap } = wrapperRef;
if (wrap && !wrap.contains(event.target)) {
setDisplay(false);
}
};
const updatePokeDex = poke => {
setSearch(poke);
setDisplay(false);
};
return (
<div ref={wrapperRef} className="flex-container flex-column pos-rel">
<input
id="auto"
onClick={() => setDisplay(!display)}
placeholder="Type to search"
value={search}
onChange={event => setSearch(event.target.value)}
/>
{display && (
<div className="autoContainer">
{options
.filter(({ name }) => name.indexOf(search.toLowerCase()) > -1)
.map((value, i) => {
return (
<div
onClick={() => updatePokeDex(value.name)}
className="option"
key={i}
tabIndex="0"
>
<span>{value.name}</span>
<img src={value.sprite} alt="pokemon" />
</div>
);
})}
</div>
)}
</div>
);
};
function App() {
return (
<div className="App">
<h1>Custom AutoComplete React</h1>
<div className="logo"></div>
<div className="auto-container">
<Auto />
</div>
</div>
);
}
export default App;
Please Help me to figure out this.
Sounds more like a CSS problem if I'm understnading it correctly. Try adding this to your ./App.css file:
.autoContainer{
max-height: 350px; overflow-y: auto; overflow-x: hidden;
}
Here is the idea: https://codesandbox.io/s/green-flower-zeued?file=/src/App.js:1509-1522
I am working on one React infinite scroll component and its working fine and perfectly, only one small issue is that for page number 1, it makes twice the API request, which I do not want and for other pages ex 2,3,4 it makes request only once.
I tried everything but i am unable to modify the code so that for page number 1,it makes only once the request.
How can i make only once the request for page number 1 also ?
here is the working code.
import React, { useState, useEffect } from "react";
import axios from "axios";
function Lists() {
const [posts, setPosts] = useState([]);
const [newposts, setNewposts] = useState([]);
const [isFetching, setIsFetching] = useState(false);
const [page, setPage] = useState(1);
const LIMIT = 7;
const getPosts = () => {
axios.get(`https://jsonplaceholder.typicode.com/posts?_limit=${LIMIT}&_page=${page}`)
.then(res => {
setNewposts(res.data);
setPosts([...posts, ...res.data]);
setIsFetching(false);
})
};
const getMorePosts= () => {
setPage(page + 1);
getPosts();
}
const handleScroll = () => {
if (
window.innerHeight + document.documentElement.scrollTop !==
document.documentElement.offsetHeight
) return;
setIsFetching(true);
}
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
useEffect(() => {
getPosts();
},[]);
useEffect(() => {
if (!isFetching){
return;
}
if( newposts.length > 0 ){
getMorePosts();
}
}, [isFetching]);
return (
<div className="App">
{posts.map((post, index) => (
<div key={index} className="post">
<div className="number">{post.id}</div>
<div className="post-info">
<h2 className="post-title">{post.title}</h2>
<p className="post-body">{post.body}</p>
</div>
</div>
))}
{isFetching && newposts.length > 0 && (
<div style = {{display: "flex", justifyContent:"center"}}>
<div className="spinner-border" role="status">
<span className="sr-only">Loading...</span>
</div>
</div>
)}
</div>
);
}
export default Lists;
The issue is that setPage, like any state setter, works asynchronously - it doesn't update the value of page immediately like it appears you might be expecting it to. The value of page will only be updated in a subsequent render which is triggered by the state change.
That means that in getMorePosts() you're not really getting page + 1, you're just getting page.
That means on first render you're calling getPosts() in that other useEffect, with page set to 1, then calling it again when you scroll and call getPosts() inside the getMorePosts() call.
Subsequent calls only happen once because getMorePosts() increments page in every subsequent render.
As for a fix, a quick one might be to just take page as an arg to getPosts, then you can statically call getPosts(1) on first render and keep the rest of the logic the same but init the page state to 2 and change the call to getPage(page) inside getMorePosts().