I was wonder what I'm doing wrong here.
I'm getting this error: "Rendered more hooks than during the previous render."
export default function ProductDetails() {
//Use State
const {qty, increaseQty, decreaseQty, onAdd, setQty} = useStateContext();
//Reset Qty
useEffect(() => {
setQty(1);
}, []);
//Fetch Slug
const {query} = useRouter();
//Fetch Graphql data
const [results] = useQuery({
query: GET_PRODUCT_QUERY,
variables: {slug: query.slug}
})
const {data, fetching, error} = results;
//Check for data coming in
if(fetching) return <p>Loading...</p>;
if(error) return <p>Oh no....</p>;
//Extract Product Data
const {title,description, image, gallery } = data.products.data[0].attributes;
const [img, setImg] = useState(gallery.data[0].attributes.formats.medium.url);
console.log(img);
//Create a toast
const notify = () => {
toast.success(`${title} added to your cart`, {duration: 1500});
}
return(
<DetailsStyle>
<Gallery>
<img src={gallery.data[0].attributes.formats.medium.url} alt={title} />
<Thumbnails>
{gallery.data.map((image, index) => (
<SingleThumb key={index} >
<img src={image.attributes.formats.thumbnail.url} alt={title} />
</SingleThumb>
)
)}
</Thumbnails>
</Gallery>
<ProductInfo>
<h3>{title}</h3>
<p>{description}</p>
<Quantity>
<span>Quantity</span>
<button><AiFillMinusCircle onClick={decreaseQty} /></button>
<p>{qty}</p>
<button><AiFillPlusCircle onClick={increaseQty}/></button>
</Quantity>
<Buy onClick={() => {
onAdd(data.products.data[0].attributes, qty)
notify();
}}>Add To Cart</Buy>
</ProductInfo>
</DetailsStyle>
)
}
Something wrong is in this line: const [img, setImg] = useState();
Why I can't use more hooks here.
Does anyone know why I'm getting this?
You are using early return
and this line of code won't execute every time:
const [img, setImg] = useState(gallery.data[0].attributes.formats.medium.url);
This is only conditionally called:
const [img, setImg] = useState(gallery.data[0].attributes.formats.medium.url);
Because the component has earlier conditional return statements. Move it to earlier in the function. (Generally I invoke useState operations right away.)
Hooks need to always be consistently called in the same order on every render.
You declare your state after some return statements. It means that if you had any errors or you were in loading state, the state is not defined. But maybe in the next render, the data is set and then your state will be defined with the inital value (gallery.data[0].attributes.formats.medium.url).
It's forbidden in react because all of the hooks should always be in the same order on every single render. In order to fix this, you should change the place of your useState for img.
Hope it helps:
export default function ProductDetails() {
const [img, setImg] = useState('');
//Use State
const {qty, increaseQty, decreaseQty, onAdd, setQty} = useStateContext();
//Reset Qty
useEffect(() => {
setQty(1);
}, []);
//Fetch Slug
const {query} = useRouter();
//Fetch Graphql data
const [results] = useQuery({
query: GET_PRODUCT_QUERY,
variables: {slug: query.slug}
})
const {data, fetching, error} = results;
//Check for data coming in
//Extract Product Data
useEffect(() => {
if(results && results.data) {
const {data} = results
const { gallery } = data.products.data[0].attributes;
setImg(gallery.data[0].attributes.formats.medium.url);
}
}, [results]);
useEffect(() => {
console.log(img);
}, [img]);
//Create a toast
const notify = (title) => {
toast.success(`${title} added to your cart`, {duration: 1500});
}
if(fetching) {
return <p>Loading...</p>;
} else if(error) {
return <p>Oh no....</p>;
} else if(data) {
const { title, description, image, gallery } = data.products.data[0].attributes;
return(
<DetailsStyle>
<Gallery>
<img src={gallery.data[0].attributes.formats.medium.url} alt={title} />
<Thumbnails>
{gallery.data.map((image, index) => (
<SingleThumb key={index} >
<img src={image.attributes.formats.thumbnail.url} alt={title} />
</SingleThumb>
)
)}
</Thumbnails>
</Gallery>
<ProductInfo>
<h3>{title}</h3>
<p>{description}</p>
<Quantity>
<span>Quantity</span>
<button><AiFillMinusCircle onClick={decreaseQty} /></button>
<p>{qty}</p>
<button><AiFillPlusCircle onClick={increaseQty}/></button>
</Quantity>
<Buy onClick={() => {
onAdd(data.products.data[0].attributes, qty)
notify(title);
}}>Add To Cart</Buy>
</ProductInfo>
</DetailsStyle>
)
} else {
return null;
}
}
There should be no return before hooks.
These lines
if(fetching) return <p>Loading...</p>;
if(error) return <p>Oh no....</p>;
should be after all hooks
Related
I got error: 'Invalid time value' when using formatDistanceToNow from data-fns.
post.createdAt date format: 2022-09-10T18:12:10.072Z
Here is my code:
import formatDistanceToNow from 'date-fns/formatDistanceToNow'
export const SinglePost = () => {
const { id } = useParams()
const [post, setPost] = useState({})
useEffect(() => {
const fetchPost = async () => {
const response = await fetch(`/api/posts/${id}`)
const data = await response.json()
if (response.ok) {
setPost(data)
}
}
fetchPost()
}, [])
return (
<Flexbox >
<SinglePostStyle className='flex-item'>
<h1 className='post-title'>{post.title}</h1>
<p className="post-author">By: {post.author}</p>
// Invalid time value
<p className="post-date">{formatDistanceToNow(new Date(post.createdAt))}</p>
<img className='post-image' src={post.image} alt="" />
<p className='post-description'>{post.description}</p>
</SinglePostStyle>
<Sidebar className='flex-item' />
</Flexbox >
)
}
if you have any suggestion please let me know, thanks
Edited: I found the solution, i need to check if there is post.createdAt comming from request or not because first render post.createdAt is undefined.
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
I am building Weather App, my idea is to save city name in localStorage, pass a prop to child component, then iterate using map and display each in seperate child of the first child
The problem is that displayed data doubles/triples on render(depending on component when render occurs) so when I have for example city London and add city Berlin it will render:
London,London,Berlin
The problem is not in AddCity component, it's working correctly but in this mix of asynchronous setState/fetching and maping
Please see the code below
App(parent component)
const App = () => {
const [cities, setCities] = useState([]);
const addCity = (newCity)=>{
console.log('adding')
setCities([...cities, newCity]);
let cityId = localStorage.length;
localStorage.setItem(`city${cityId}`, newCity);
}
useEffect(() => {
loadCityFromLocalStore()
}, [])
const loadCityFromLocalStore =()=>{
setCities([...cities, ...Object.values(localStorage)])
}
return (
<div>
<Header />
<AddCity addCity={addCity}/>
<DisplayWeather displayWeather={cities}/>
</div>
)
}
DisplayWeather (first child)
const DisplayWeather = ({displayWeather}) => {
const apiKey = '4c97ef52cb86a6fa1cff027ac4a37671';
const [fetchData, setFetchData] = useState([]);
useEffect(() => {
displayWeather.map(async city=>{
const res =await fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${apiKey}`)
const data = await res.json();
setFetchData((fetchData=>[...fetchData , data]));
})
}, [displayWeather])
return (
<>
{fetchData.map(data=>(
<ul>
<Weather
data={data}/>
</ul>
))}
</>
)
}
Weather component
const Weather = ({data}) => {
return (
<li>
{data.name}
</li>
)
}
It looks like the problem comes from calling setFetchData for cities that you already added previously.
One easy way to fix it would be to store fetch data as an object instead of a dictionary so that you just override the data for the city in case it already exists (or maybe even skip the fetch as you already have the data).
For example:
const [fetchData, setFetchData] = useState({});
useEffect(() => {
displayWeather.map(async city=>{
const res = await fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${apiKey}`)
const data = await res.json();
setFetchData((fetchData=> ({...fetchData, [city]: data})));
})
}, [displayWeather])
Then, to map over fetch data you can just use Object.values:
return (
<>
{Object.values(fetchData).map(data=>(
<ul>
<Weather
data={data}/>
</ul>
))}
</>
)
If you want to skip already fetched cities you can do something like this instead:
useEffect(() => {
displayWeather.map(async city=>{
if (!fetchData[city]) {
const res = await fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${apiKey}`)
const data = await res.json();
setFetchData((fetchData=> ({...fetchData, [city]: data})));
}
})
I'm putting navbar in my _app.js so I don't need to insert it in every component. My problem is that after I login it outputs an error Rendered more hooks than during the previous render. and its pointing it on useQuery(GETCARTDATA
Pls check my code here
const App = ({ Component, pageProps }) => {
const token = getToken()
const [isPopUpShow, setPopUpShow] = useState(false)
const [cartStateData, setCartStateData] = useState([])
const [isCartOpen, setCartOpen] = useState(false)
let cartDetailsData
if (token) {
// eslint-disable-next-line react-hooks/rules-of-hooks
cartDetailsData = useLazyQuery(GETCARTDATA, {
variables: {
page: 1
},
})
// eslint-disable-next-line react-hooks/rules-of-hooks
useMemo(() => {
const cartData = get(cartDetailsData.data, 'findCartDetails.orders') || []
const cartItems = []
if (cartData.length) {
cartData.map(
itm =>
itm.lineItems.length &&
itm.lineItems.map(item => cartItems.push(item))
)
}
setCartStateData(cartItems)
}, [cartDetailsData.data])
}
return (
<>
<div className="app-outer">
{token ? (
<ShowroomHeader
isPopUpShow={isPopUpShow}
setPopUpShow={setPopUpShow}
cartStateData={cartStateData}
cartDetailsData={cartDetailsData}
token={token}
/>
) : (
<Navbar />
)}
</div>
<div className="main">
<Component {...pageProps} />
</div>
</>
)
}
export default withApollo(App)
As #xadmn mentioned, you're rendering your hooks conditionally while React expects the same number of hook calls on every render, thus breaking the rules of Hooks.
You'll need to remove your if statement and move your condition inside a useEffect hook, using useLazyQuery's returned function to execute the query from there. You can also move your useMemo code to the onCompleted callback, since it depends on the results from the query.
const App = ({ Component, pageProps }) => {
const token = getToken()
const [isPopUpShow, setPopUpShow] = useState(false)
const [cartStateData, setCartStateData] = useState([])
const [isCartOpen, setCartOpen] = useState(false)
const [getCardData, cartDetailsData] = useLazyQuery(GETCARTDATA, {
onCompleted: (data) => {
const cartData = get(data, 'findCartDetails.orders') || []
const cartItems = []
if (cartData.length) {
cartData.map(
itm =>
itm.lineItems.length &&
itm.lineItems.map(item => cartItems.push(item))
)
}
setCartStateData(cartItems)
}
})
useEffect(() => {
if (token) {
getCardData({ variables: { page: 1 } })
}
}, [token])
return (
// Your JSX here
)
}
I am trying to create a search filter for countries. I search a country and display their information and weather of country's capital using a weather api. I am fetching the data of a country using axios but the response.data is undefined and hence its cause error.
I know the code is async. So how do I fetch data from url before I setWeather(response.data) .
const Weather = ({capital}) => {
const [weather, setWeather] = useState([])
const key = 'mykey'
const url = `http://api.weatherstack.com/current?access_key=${key}&query=${capital}`
axios.get(url)
.then(response => {
console.log('promise fullfilled')
setWeather(response.data)
})
return(
<div>
<h1>Weather in {weather.location.name}</h1>
<h2>temperature: {weather.current.temperature} </h2>
<img src = {weather.current.weather_icons} />
<h2>wind: {weather.current.wind_speed} kph direction {weather.current.wind_dir}</h2>
</div>
)
}
const PrintLanguages = ({lang}) =>{
return(
lang.map(l => <li key={l}>{l}</li>)
)
}
const View = ({country}) =>{
const lang = country.languages.map(lang => lang.name)
return(
<div>
<h1>{country.name}</h1>
<p>capital {country.capital}</p>
<p>population {country.population}</p>
<h2>languages</h2>
<ul><PrintLanguages lang={lang}/></ul>
<img src={country.flag} alt="flag photo" height="100" width="100"/>
<Weather capital={country.capital}/>
</div>
)
}
I expected this result but instead I am getting this Type Error
Please guide me on how to fix this ??
You can use the effect hook here:
const Weather = ({capital}) => {
const [weather, setWeather] = useState({location:{}, current: {}});
const key = 'mykey'
const url = `http://api.weatherstack.com/current?access_key=${key}&query=${capital}`
useEffect(() => {
axios.get(url)
.then(response => {
console.log('promise fullfilled')
setWeather(response.data)
})
}, [capital]) // Fetch the data when capital changes
return(
<div>
<h1>Weather in {weather.location.name}</h1>
<h2>temperature: {weather.current.temperature} </h2>
<img src = {weather.current.weather_icons} />
<h2>wind: {weather.current.wind_speed} kph direction {weather.current.wind_dir}</h2>
</div>
)
}
This will call the api method after the component is mounted.
Also make sure your initial state structure is the same as the one for the rendered state. In your case you set it to an empty array but when rendering it expects an object.
One more way is to use a loading state, during which you can show a loading indicator before the data fetches:
const Weather = ({capital}) => {
const [weather, setWeather] = useState({location:{}, current: {}});
const [loading, setLoading] = useState(true);
const key = 'mykey'
const url = `http://api.weatherstack.com/current?access_key=${key}&query=${capital}`
useEffect(() => {
axios.get(url)
.then(response => {
console.log('promise fullfilled')
setLoading(false);
setWeather(response.data)
})
}, [])
return loading ? <p>Loading...</p> : (
<div>
<h1>Weather in {weather.location.name}</h1>
<h2>temperature: {weather.current.temperature} </h2>
<img src = {weather.current.weather_icons} />
<h2>wind: {weather.current.wind_speed} kph direction {weather.current.wind_dir}</h2>
</div>
)
}