I was working on a project and testing different data fetching techniques in Next.js. I encountered the issue of Hydration Error on the home page, where I used getStaticProps. However, on a different page where I used fetch(), no errors appeared.
I found a workaround by using setTimeout and useEffect to wait for the component to render, which resolved the Hydration Error.
My question is, why did waiting for the component to render potentially solve the issue, but not having that wait caused the Hydration Error?
Code Snippets below:
Home Page
const [isLoading, setLoading] = useState(true);
useEffect(() => {
setTimeout(() => {
setLoading(false);
}, 500);
}, [])
<Tabs redirect="/matches" showall={true}>
<Tab title="National League">
{isLoading ? <h5>Loading...</h5> :
<Matches props={props.fixtures} cid="National League" status="Not Started"></Matches>
}
</Tab>
<Tab title="Louis Borg Cup">
{isLoading ? <h5>Loading...</h5> :
<Matches props={props.fixtures} cid="Louis Borg Cup" status="Not Started"></Matches>
}
</Tab>
<Tab title="Friendlies">
{isLoading ? <h5>Loading...</h5> :
<Matches props={props.fixtures} cid="Friendlies" status="Not Started"></Matches>
}
</Tab>
</Tabs>
Other Page
function MatchesPage(){
const [isLoading, setLoading] = useState(true);
const [fixtures, setFixturesData] = useState<MatchType[]>([]);
const [results, setResultsData] = useState<MatchType[]>([]);
useEffect(() => {
async function fetchMatchesData() {
const response = await fetch('/api/matches');
const data = await response.json()
const modifiedData = data.map((result: MatchType) => {
const startDateString = result.startDate.toString();
const endDateString = result.endDate.toString();
const date = new Date(startDateString);
var formattedDate = date.toLocaleString('en-UK', { day: 'numeric', month: "long", hour: 'numeric', minute: 'numeric' });
formattedDate = formattedDate.replace(",", " /");
return{
...result,
startDate: startDateString,
endDate: endDateString,
formattedDate: formattedDate
}
})
const modifiedResults = modifiedData.filter((md: MatchType) => md.status === "Finished")
const modifiedFixtures = modifiedData.filter((md: MatchType) => md.status === "Not Started")
setFixturesData(modifiedFixtures);
setResultsData(modifiedResults);
setTimeout(function(){
setLoading(false);
}, 2000);
}
fetchMatchesData()
}, [])
Related
I have a hook that rules my requests. It has state "loading" that becoming true while loading
export const useHttp = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const request = useCallback(async (url, method = 'GET', body = null, headers = {'Content-Type': 'application/json' }) => {
setLoading(true);
try {
const res = await fetch(url, {method, body, headers});
if (!res.ok) {
throw new Error(`Could not fetch ${url}, status: ${res.status}`);
}
const data = await res.json();
setLoading(false);
return data;
} catch(e) {
setLoading(false);
setError(e.message);
throw e;
}
}, [])
const clearError = useCallback(() => {
setError(null);
}, [])
return {
loading,
error,
request,
clearError
}
}
Also i have a service that makes requests:
import { useHttp } from "../hooks/http.hook";
const useNasaService = () => {
const { loading, error, request, clearError } = useHttp();
const _apiBase = 'https://api.nasa.gov/';
const _apiKey = 'api_key=DEMO_KEY';
const getMissionManifest = async (rover) => {
const res = await request(`${_apiBase}mars-photos/api/v1/manifests/${rover}/?${_apiKey}`);
return _transformManifestData(res.photo_manifest);
}
const getImagesData = async (rover, sol, page = 1) => {
const res = await request(`${_apiBase}mars-photos/api/v1/rovers/${rover}/photos?sol=${sol}&page=${page}&${_apiKey}`);
return res.photos.map(_transformImagesData);
}
const _transformImagesData = (data) => {
return {
id: data.id,
sol: data.sol,
earthDate: data.earth_date,
path: data.img_src,
camera: data.camera.full_name,
rover: data.rover.name
}
}
const _transformManifestData = (data) => {
return {
landingDate: data.landing_date,
launchDate: data.launch_date,
maxDate: data.max_date,
maxSol: data.max_sol,
name: data.name,
photos: data.photos,
status: data.status,
totalPhotos: data.total_photos
}
}
return {
loading,
error,
clearError,
getImagesData,
getMissionManifest
}
}
export default useNasaService;
Finally i have a component that needs state "loading" for disabling the inputs.
The question is why "loading" is never getting true in this component:
import useNasaService from '../../services/useNasaService';
const RoverFilter = (props) => {
const { loading } = useNasaService();
console.log(loading); /* always false */
const onRadioChange = (e) => {
props.onRoverSelected(e.target.value);
props.onRoverClicked(e.target.value);
}
return (
<div className="roverFilter" >
<h2 className="roverFilter__title">Select rover</h2>
<div className="roverFilter__inputs">
<label htmlFor="curiosity">Curiosity</label>
<input disabled={loading} type="radio" name="rover-choise" id="curiosity" value="curiosity" onChange={onRadioChange}/>
<label htmlFor="opportunity">Opportunity</label>
<input disabled={loading} type="radio" name="rover-choise" id="opportunity" value="opportunity" onChange={onRadioChange}/>
<label htmlFor="spirit">Spirit</label>
<input disabled={loading} type="radio" name="rover-choise" id="spirit" value="spirit" onChange={onRadioChange}/>
<label htmlFor="perseverance">Perseverance</label>
<input disabled={loading} type="radio" name="rover-choise" id="perseverance" value="perseverance" onChange={onRadioChange}/>
</div>
</div>
)
}
export default RoverFilter;
By the way, in my app there are another components, where "loading" becoming true without any problems. I cant see the difference.
for example, here loading works good:
import { useEffect, useState } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import useNasaService from '../../services/useNasaService';
import ImageGallerySkeleton from '../imageGallerySkeleton/ImageGallerySkeleton';
import Spinner from '../spinner/Spinner';
import ErrorMessage from '../errorMessage/ErrorMessage';
import SliderModal from '../sliderModal/SliderModal';
const ImageGallery = (props) => {
const {loading, getImagesData, clearError, error} = useNasaService();
const [imagesData, setImagesData] = useState([]);
const [nextPage, setNextPage] = useState(1);
const [firstLoading, setFirstLoading] = useState(true);
const [imagesDataLoaded, setImagesDataLoaded] = useState(false);
const [itemIndex, setItemIndex] = useState(0);
const [sliderOpen, setSliderOpen] = useState(false);
const transitionDuration = 1000;
const onImagesDataLoaded = (newData) => {
setImagesData(data => [...data, ...newData]);
setNextPage(page => page + 1);
setFirstLoading(false);
setImagesDataLoaded(true);
}
const onRequestImages = (rover, sol, page) => {
clearError();
if (!rover || !sol) return;
getImagesData(rover, sol, page)
.then(onImagesDataLoaded);
}
const onSliderClosed = () => {
setSliderOpen(false);
}
useEffect(() => {
onRequestImages(props.selectedRover, props.selectedSol, nextPage);
// eslint-disable-next-line
}, [props.selectedRover, props.selectedSol])
if (sliderOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "visible";
}
function renderItemList(arr) {
const itemList = arr.map((item, i) => {
return (
<CSSTransition
key={item.id}
in={imagesDataLoaded}
timeout={transitionDuration}
classNames='imageGallery__card'>
<li className="imageGallery__card"
onClick={() => {
setSliderOpen(true);
setItemIndex(i);
}}>
<img src={item.path} alt="img from mars"/>
<div className="imageGallery__descr">
<ul>
<li>Rover: {item.rover}</li>
<li>Earth_date: {item.earthDate}</li>
<li>Sol: {item.sol}</li>
<li>{item.camera}</li>
</ul>
</div>
</li>
</CSSTransition>
)
})
return (
<ul className="imageGallery__list">
<TransitionGroup component={null}>
{itemList}
</TransitionGroup>
</ul>
)
}
const spinner = loading && firstLoading ? <Spinner/> : null;
const skeleton = imagesData.length === 0 && firstLoading && !loading && !error ? <ImageGallerySkeleton/> : null;
const items = renderItemList(imagesData);
const errorMessage = error ? <ErrorMessage/> : null;
const counter = imagesData.length === 0 || error ? null :
<h2 className="imageGallery__title">
Showed {loading ? "..." : imagesData.length} photos of {props.totalPhotosInSol}
</h2>
const button = props.totalPhotosInSol === imagesData.length ? null :
<button
onClick={() => onRequestImages(props.selectedRover, props.selectedSol, nextPage)}
disabled={loading}
className="imageGallery__btn">{loading ? "Loading..." : "Load next page" }
</button>
const slider = <SliderModal
open={sliderOpen}
items={imagesData}
slideIndex={itemIndex}
onSliderClosed={onSliderClosed} />
const wrapStyles = firstLoading && loading ? {"padding": "50px"} : null;
return (
<section className="imageGallery" style={wrapStyles}>
{counter}
{spinner}
{skeleton}
{imagesData.length === 0 && !firstLoading ?
<h2 className="imageGallery__title">There is no photo for this sol</h2> :
items
}
{button}
{errorMessage}
{slider}
</section>
)
}
export default ImageGallery;
When you call useState in two different components, those states are independant from eachother. This is still true if you move the useState calls inside a custom hook. If two components call useNasaService (which calls useHttp, which calls useState), then the two components are creating their own states and own functions. If component A starts loading data, that will have no effect on component B.
So ImageGallery is working because it makes a call to getImagesData. This sets the loading state of ImageGallery to true. No other components are affected by this though. When the loading finishes, ImageGallery will set state to have the new data, but again, no other components can use this. RoverFilter on the other hand never calls getImagesData, so its loading state stays false, and it never gets any data.
In react, the typical way to share data is to lift state up. You have a component higher up in the tree, which is responsible for loading the data and setting state. That component then passes the data and functions down to any children that need it. You can either pass the data down using props, or if you need to pass the data a long distance you can consider using context instead.
There's also a wide variety of 3rd party libraries which can be used to manage global state. For example, Redux, Jotai, Zustand, MobX. These can make it simpler to share data between components in far-flung parts of the component tree.
I have this Dropdown Menu (done with MUI) which allows to choose the day value. When it changes, I'd like it to make a new GET request with the new parameter, but I don't know how to do it as it uses useEffect.
My function to fetch data
const [loading, setLoading] = useState(true);
const [prepData, setPrepData] = useState([]);
const [day, setDay] = React.useState(3);
console.log(day);
const options=["J+1","J+2","J+3", "J+4"]
const handleChange = (event) => {
setDay(event.target.value);
};
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const {data: response} = await axios.get('/api/home?day=' + day)
setPrepData(response)
} catch (err) {
console.log(err.message)
}
setLoading(false)
}
fetchData()
}, []);
My dropdown menu :
<Box key={'box' + index} sx={{ minWidth: 120 }}>
<FormControl key={'form' + index} fullWidth>
<InputLabel key={'input' + index} id="dropdown">Date Liv.</InputLabel>
<Select
key={'select' + index}
labelId="select-label"
id="dateLiv"
value={day}
label="Date Liv."
onChange={handleChange}
type='submit'
>
{(options).map((option, index) => (
<MenuItem key={'menuItem' + index} value={index + 1}>{option}</MenuItem>
))}
</Select>
</FormControl>
</Box>
You can add day as dependency in useEffect. So that when day value is changed, automatically useEffect will be executed.
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const {data: response} = await axios.get('/api/home?day=' + day)
setPrepData(response)
} catch (err) {
console.log(err.message)
}
setLoading(false)
}
fetchData()
}, [day]); // added day as dependent property
Add as a dependency:
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const {data: response} = await axios.get('/api/home?day=' + day)
setPrepData(response)
} catch (err) {
console.log(err.message)
}
setLoading(false)
}
fetchData()
}, [day]); // ------> here
The useEffect you defined is triggered when your component is mounting for the first time cause you use the [] parameter at the end. But you can give any state var instead of it.
If you want to refresh the data each time your dropdown value change, you can declare another useEffect which is called as soon as your "day" state var changes. Do something like this :
useEffect(() => {
// Action called
fetchData(day)
}, [day])
I'm using react-intersection-observer npm package for my CRA project.
I want to achieve this Infinite Scroll effect whenever users reach the bottom of the page or click on the Load More button -> It will fetch 8 more items.
Currently, the getMoreData() will only execute when I click on the Load More button.
Also, I put posts state in the dependency array so it will display the first 8 items for activePosts on start. But this leads to status code 429: over rate limit in the Network tab.
My Questions:
How to also apply the getMoreData() function to fetch out more items when we scroll to the end of the page? (Infinity scroll)
There might be a bug if I remove the posts from the dependency array, it won't display the first 8 items on initial page load. How to fix this the right way?
Screenshots:
My Code:
Posts.js
function Posts() {
const url = "https://6264f60294374a2c506b97c9.mockapi.io/posts";
const [posts, setPosts] = useState([]);
const [activePosts, setActivePosts] = useState([]);
const [isFetching, setIsFetching] = useState(false);
const getData = async () => {
try {
let response = await axios(url);
let result = response.data;
setPosts(result);
setActivePosts(posts.slice(0, 8));
} catch (err) {
console.log(err);
}
};
useEffect(() => {
getData();
}, [posts]);
const getMoreData = () => {
setIsFetching(true);
setTimeout(() => {
setActivePosts((prev) => {
return [...prev, ...posts.slice(prev.length + 1, prev.length + 9)];
});
setIsFetching(false);
}, 2000);
};
useEffect(() => {
if (!isFetching) return;
getMoreData();
}, [isFetching]);
return (
<>
<div className="posts">
{activePosts.map((post, index) => (
<Post post={post} key={post.id} index={index} />
))}
</div>
<button onClick={getMoreData}>
{isFetching ? "Loading..." : "Load more"}
</button>
</>
);
}
export default Posts;
Post.js
import { useInView } from "react-intersection-observer";
function Post({ post }) {
const { ref, inView } = useInView({
initialInView: true,
triggerOnce: true,
threshold: 1,
});
return (
<div className="post" ref={ref}>
{inView ?
<img src={post.imgUrl} alt={post.title} className="post__img" loading="lazy" />
: <div className="post__img" />
}
<h1 className="post__title">{post.title}</h1>
</div>
)
}
export default Post
Intro
I had created a NewsReaderApp which is working fine in the local environment using newsAPI.
Problem
The problem is occurring in the deployment part, it is deployed successfully in Netlify but when I open the deployed site click here , it is showing no news articles,
ideally, it should look like this picture below
.
This means that the API is not fetching the data. I don't know why it is happening. Even I tried to console log the parsedData I am not able to see the consoled stmt in the local as you can see in the pic also. I am sharing the code of the file where I am fetching the data.
code
News.js
import React, { useState,useEffect } from "react";
import NewItem from "./NewItem";
import Spinner from "./Spinner";
import InfiniteScroll from "react-infinite-scroll-component";
const News = (props) => {
const capitilizeFirstLetter = (str) => {
return str.charAt(0).toUpperCase() + str.slice(1);
};
const [articles, setArticles] = useState([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [totalResults, setTotalResults] = useState(0);
const updateNews = async() => {
props.setProgress(10);
const url = `https://newsapi.org/v2/top-headlines?country=${props.country}&category=${props.category}&apiKey=${props.apikey}&page=${page}&pageSize=${props.pageSize || "12"}`;
setLoading(true);
let data = await fetch(url);
props.setProgress(30);
let parsedData = await data.json();
props.setProgress(50);
//console.log(parsedData)
setArticles(parsedData.articles);
setTotalResults(parsedData.totalResults);
setLoading(false);
props.setProgress(100);
}
useEffect(() => {
updateNews();
document.title = `NewsReader2 - ${capitilizeFirstLetter(props.category)}`;
// eslint-disable-next-line react-hooks/exhaustive-deps
//console.log("render");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const fetchMoreData = async () => {
let url = `https://newsapi.org/v2/top-headlines?country=${props.country || "in"}&category=${props.category || "general"}&apiKey=${props.apikey}&page=${page+1}&pageSize=${props.pageSize}`;
setPage(page+1);
let data = await fetch(url);
let parsedData = await data.json();
console.log(parsedData)
setArticles(articles?.concat(parsedData?.articles));
setTotalResults(parsedData.totalResults);
};
return (
<div style={props.darkmodeStyle}>
<h1 className="text-center" style={{ margin: "35px 0px", marginTop:'5%' }}>
New Reader2
</h1>
<div
className={`alert alert-${props.mode === 'light' ? 'primary':'custom'} text-center`}
role="alert"
style={{ fontSize: "150%", fontWeight: 700 }}
>
News from {capitilizeFirstLetter(props.category)} world
</div>
{loading && <Spinner />}
<InfiniteScroll
next={fetchMoreData}
dataLength={
articles?.length ? articles?.length : 15
}
hasMore={articles?.length !== totalResults}
loader={<Spinner />}
>
<div className="container">
<div className="row">
{articles?.map((item) => {
return (
<div className="col-md-4" key={item.url}>
<NewItem
title={item.title ? item.title.slice(0, 45) : ""}
description={
item.description ? item.description.slice(0, 88) : ""
}
imageUrl={
item.urlToImage
? item.urlToImage
: "https://www.eastmojo.com/wp-content/uploads/2021/08/space-pen-5.jpg"
}
newsUrl={item.url}
publishedAt={item.publishedAt}
author={item.author ? item.author : "Anonymous"}
source={item.source.name}
darkmodeStyle={props.darkmodeStyle}
/>
</div>
);
})}
</div>
</div>
</InfiniteScroll>
</div>
);
}
export default News;
Please help me to find a way to deploy this app successfully?
You have CORS issue. The API you are using worked on development because the domain localhost is allowed. See the image below
The error message is : "Requests from the browser are not allowed on the Developer plan, except from localhost."
Check out the API's docs to see what you can do. It seems that you need to pay in order to deploy your website.
I have a React component using an infinite scroll to fetch information from an api using a pageToken.
When the user hits the bottom of the page, it should fetch the next bit of information. I thought myself clever for passing the pageToken to a useEffect hook, then updating it in the hook, but this is causing all of the api calls to run up front, thus defeating the use of the infinite scroll.
I think this might be related to React's derived state, but I am at a loss about how to solve this.
here is my component that renders the dogs:
export const Drawer = ({
onClose,
}: DrawerProps) => {
const [currentPageToken, setCurrentPageToken] = useState<
string | undefined | null
>(null);
const {
error,
isLoading,
data: allDogs,
nextPageToken,
} = useDogsList({
pageToken: currentPageToken,
});
const loader = useRef(null);
// When user scrolls to the end of the drawer, fetch more dogs
const handleObserver = useCallback(
(entries) => {
const [target] = entries;
if (target.isIntersecting) {
setCurrentPageToken(nextPageToken);
}
},
[nextPageToken],
);
useEffect(() => {
const option = {
root: null,
rootMargin: '20px',
threshold: 0,
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
}, [handleObserver]);
return (
<Drawer
onClose={onClose}
>
<List>
{allDogs?.map((dog) => (
<Fragment key={dog?.adopterAttributes?.id}>
<ListItem className={classes.listItem}>
{dog?.adopterAttributes?.id}
</ListItem>
</Fragment>
))}
{isLoading && <div>Loading...</div>}
<div ref={loader} />
</List>
</Drawer>
);
};
useDogsList essentially looks like this with all the cruft taken out:
import { useEffect, useRef, useState } from 'react';
export const useDogsList = ({
pageToken
}: useDogsListOptions) => {
const [isLoading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [nextPageToken, setNextPageToken] = useState<string | null | undefined>(
null,
);
const [allDogs, setAllDogs] = useState(null);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const result =
await myClient.listDogs(
getDogsRequest,
{
token,
},
);
const dogListObject = result?.toObject();
const newDogs = result?.dogsList;
setNextPageToken(dogListObject?.pagination?.nextPageToken);
// if API returns a pageToken, that means there are more dogs to add to the list
if (nextPageToken) {
setAllDogs((previousDogList) => [
...(previousDogList ?? []),
...newDogs,
]);
}
}
} catch (responseError: unknown) {
if (responseError instanceof Error) {
setError(responseError);
} else {
throw responseError;
}
} finally {
setLoading(false);
}
};
fetchData();
}, [ pageToken, nextPageToken]);
return {
data: allDogs,
nextPageToken,
error,
isLoading,
};
};
Basically, the api call returns the nextPageToken, which I want to use for the next call when the user hits the intersecting point, but because nextPageToken is in the dependency array for the hook, the hook just keeps running. It retrieves all of the data until it compiles the whole list, without the user scrolling.
I'm wondering if I should be using useCallback or look more into derivedStateFromProps but I can't figure out how to make this a "controlled" component. Does anyone have any guidance here?
I suggest a small refactor of the useDogsList hook to instead return a hasNext flag and fetchNext callback.
export const useDogsList = ({ pageToken }: useDogsListOptions) => {
const [isLoading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [nextPageToken, setNextPageToken] = useState<string | null | undefined>(
pageToken // <-- initial token value for request
);
const [allDogs, setAllDogs] = useState([]);
// memoize fetchData callback for stable reference
const fetchData = useCallback(async () => {
setLoading(true);
try {
const result = await myClient.listDogs(getDogsRequest, { token: nextPageToken });
const dogListObject = result?.toObject();
const newDogs = result?.dogsList;
setNextPageToken(dogListObject?.pagination?.nextPageToken ?? null);
setAllDogs((previousDogList) => [...previousDogList, ...newDogs]);
} catch (responseError) {
if (responseError instanceof Error) {
setError(responseError);
} else {
throw responseError;
}
} finally {
setLoading(false);
}
}, [nextPageToken]);
useEffect(() => {
fetchData();
}, []); // call once on component mount
return {
data: allDogs,
hasNext: !!nextPageToken, // true if there is a next token
error,
isLoading,
fetchNext: fetchData, // callback to fetch next "page" of data
};
};
Usage:
export const Drawer = ({ onClose }: DrawerProps) => {
const { error, isLoading, data: allDogs, hasNext, fetchNext } = useDogsList({
pageToken // <-- pass initial page token
});
const loader = useRef(null);
// When user scrolls to the end of the drawer, fetch more dogs
const handleObserver = useCallback(
(entries) => {
const [target] = entries;
if (target.isIntersecting && hasNext) {
fetchNext(); // <-- Only fetch next if there is more to fetch
}
},
[hasNext, fetchNext]
);
useEffect(() => {
const option = {
root: null,
rootMargin: "20px",
threshold: 0
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
// From #stonerose036
// clear previous observer in returned useEffect cleanup function
return observer.disconnect;
}, [handleObserver]);
return (
<Drawer onClose={onClose}>
<List>
{allDogs?.map((dog) => (
<Fragment key={dog?.adopterAttributes?.id}>
<ListItem className={classes.listItem}>
{dog?.adopterAttributes?.id}
</ListItem>
</Fragment>
))}
{isLoading && <div>Loading...</div>}
<div ref={loader} />
</List>
</Drawer>
);
};
Disclaimer
Code hasn't been tested, but IMHO it should be pretty close to what you are after. There may be some minor tweaks necessary to satisfy any TSLinting issues, and getting the correct initial page token to the hook.
While Drew and #debuchet's answers helped me improve the code, the problem around multiple renders ended up being solved by tackling the observer itself. I had to disconnect it afterwards
useEffect(() => {
const option = {
root: null,
rootMargin: '20px',
threshold: 0,
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
return () => {
observer.disconnect();
};
}, [handleObserver]);