I have a Alice-Carousel in react where I am getting the items from an API.After fetching the data from API , I am updating the items array for the carousel but am getting the value of items as undefined. CryptoState is the context to prevent prop-drilling.
import React from 'react'
import { useEffect ,useState} from 'react';
import { CryptoState } from '../CryptoContext'
import { TrendingCoins } from '../api';
import axios from 'axios'
import AliceCarousel from 'react-alice-carousel';
import {Link} from 'react-router-dom'
const Carousel = () => {
const [trending,setTrending] = useState([]);
const {currency,setCurrency} = CryptoState();
useEffect(() => {
const {data}= axios.get(TrendingCoins(currency));
setTrending(data);
},[currency,trending])
// 0 pe 2 items dikhane hai, 512 size pe 4 items dikhane hai,
// thats all about responsiveness.
const responsives = {
0: {
items: 2
},
512: {
items: 4
}
}
const items= trending.map((coin) => {
return (
<>
<Link to={`/coins/${coin.id}`}/>
<img
className="carouselImages"
src={coin?.image}
height="80"
style={{marginBottom:10}}
/>
</>
)
})
return (
<AliceCarousel
infinite
mouseTracking
autoPlayInterval={1000}
animationDuration={1500}
disableDotsControls
responsive={responsives}
autoPlay
items={items}
/>
)
}
export default Carousel
useEffect(()=>{
const getData = async () => {
try{
const {data} = await axios.get(TrendingCoins(currency));
setTrending(data);
}catch{}
}
getData()
}, [currency, trending])
you are not waiting for the response, also you can just write async in useEffect arrow function
edit. : https://devtrium.com/posts/async-functions-useeffect
const {data}= axios.get(TrendingCoins(currency));
the problem is axios.get is a async call so basically your are not waiting for its result, you could async/await or promises
useEffect(async ()=>{
const response = await axios.get(TrendingCoins(currency));
setTrending(response.data);
},[currency,trending])
Axios is asynchronous meaning the App needs to wait for the data to be delivered. One of the way to do this is by using Thenables.
There are some problems with the useEffect() dependecies. If you add trending as a dependency there will be an infinite loop that will probable make the browser crash. This is because the useState hook re-renders the component and changes the state and useEffect triggers when a dependency value changes.
In the return you also need to make changes. If the App does not have the available data, it cannot show it. To solve this we need use conditional statements.
Take a look
import React from 'react'
import { useEffect ,useState} from 'react';
import { CryptoState } from '../CryptoContext'
import { TrendingCoins } from '../api';
import axios from 'axios'
import AliceCarousel from 'react-alice-carousel';
import {Link} from 'react-router-dom'
const Carousel = () => {
const [trending,setTrending]=useState([]);
const {currency,setCurrency}=CryptoState();
useEffect(()=>{
axios.get(TrendingCoins(currency))
.then((data) => {
setTrending(data)
})
.catch((error) => {
console.log(error)
})
},[currency])
const responsives={ //0 pe 2 items dikhane hai, 512 size pe 4 items dikhane hai,thats all about responsiveness.
0:{
items:2,
},
512:{
items:4,
}
}
const items = trending.map((coin)=>{
return (
<>
<Link to={`/coins/${coin.id}`}/>
<img className="carouselImages"
src={coin?.image}
height="80"
style={{marginBottom:10}}
/>
</>
)
})
if(trending === undefined) return (<p>Loading data....</p>)
else return (
<AliceCarousel
infinite
mouseTracking
autoPlayInterval={1000}
animationDuration={1500}
disableDotsControls
responsive={responsives} //responsive is to generate responsiveness....
autoPlay
items={items}
/>
)
}
export default Carousel
Related
I'm trying to pass a JSON object (id) returned from an API call to another component via props and use that object(id) to fetch more data from a different endpoint. The problem is, when i pass the prop using object literal to the api, it gives an error undefined but when i console log the object(id) it works fine. What could be the issue? Just started learning React.
component passing object as prop
import axios from "axios";
import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import Cast from "./Cast";
const DetailsView = () => {
const { id } = useParams();
const [details, setDetails] = useState([]);
useEffect(() => {
axios
.get(
`https://api.themoviedb.org/3/movie/${id}?api_key=<<api_key>>&language=en-US`
)
.then((response) => {
setDetails(response.data);
});
}, []);
return (
<div className="w-full h-[650px] text-white">
<<bunch of code>>
<Cast id={details?.id}/>
</div>
);
};
export default DetailsView;
component receiving prop
import React, { useState, useEffect } from "react";
import axios from "axios";
const Cast = (props) => {
const [cast, setCast] = useState([]);
const sid = props.id;
useEffect(() => {
axios
.get(
`https://api.themoviedb.org/3/movie/${sid}/credits?api_key=<<api_key>>&language=en-US`
)
.then((response) => {
setCast(response.data.cast);
console.log(response.data.cast);
});
}, []);
console.log(sid);
return (
<div className="absolute">
{cast && cast.map((item, index) => <p className="">{item.name}</p>)}
<p>{sid}</p>
</div>
);
};
export default Cast;
It doesn't work initially but when I edit the code, since the change is happening live, it fetches the data but when I refresh the page, Axios reports an error 404
xhr.js:220 GET https://api.themoviedb.org/3/movie/**undefined**/credits?api_key=56fbaac7fd77013cc072d285a17ec005&language=en-US 404
Your id property does not exist until the API call is completed, and there is a rerender after setDetails.
You can check if id exists and based on that render your Card component. Also, looks like details is an object not an array, so I changed the useState statement to reflect that.
import axios from "axios";
import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import Cast from "./Cast";
const DetailsView = () => {
const { id } = useParams();
const [details, setDetails] = useState({});
useEffect(() => {
axios
.get(
`https://api.themoviedb.org/3/movie/${id}?api_key=<<api_key>>&language=en-US`
)
.then((response) => {
setDetails(response.data);
});
}, []);
return (
<div className="w-full h-[650px] text-white">
<<bunch of code>>
{details?.id && <Cast id={details?.id}/>}
</div>
);
};
export default DetailsView;
I'm still new to React so forgive me if this is a silly approach to this problem.
My goal: Global error handling using a context provider and a custom hook.
The Problem: I can't remove errors without them immediately being re-added.
I display my errors via this component in the shell...
import React, { useState, useEffect } from 'react'
import Alert from '#mui/material/Alert'
import Collapse from '#mui/material/Collapse'
import { useAlertContext } from '#/context/alert-context/alert-context'
export default function AppAlert () {
const [show, setShow] = useState(false)
const alertContext = useAlertContext()
const handleClose = () => {
alertContext.remove()
setShow(false)
}
useEffect(() => {
if (alertContext.alert) {
setShow(true)
}
}, [alertContext.alert])
return (
<Collapse in={show}>
<Alert severity='error' onClose={handleClose}>
{alertContext.alert}
</Alert>
</Collapse>
)
}
I have a provider setup that also exposes a custom hook...
import React, { useState, createContext, useContext } from 'react'
const AlertContext = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = () => setAlert(null)
const addAlert = (message) => setAlert(message)
return (
<AlertContext.Provider value={{
alert,
add: addAlert,
remove: removeAlert
}}
>
{children}
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
And finally I have a hook setup to hit an API and call throw errors if it any occur while fetching the data. I'm purposely triggering a 404 by passing a bad API path.
import { useEffect } from 'react'
import { useQuery } from 'react-query'
import ApiV4 from '#/services/api/v4/base'
import { useAlertContext } from '#/context/alert-context/alert-context'
export const useAccess = () => {
const alertContext = useAlertContext()
const route = '/accessx'
const query = useQuery(route, async () => await ApiV4.get(route), {
retry: 0
})
useEffect(() => {
if (query.isError) {
alertContext.add(query.error.toString())
}
}, [alertContext, query.isError, query.error])
return query
}
This code seems to be the issue. Because alertContext.remove() triggers useEffect here and query.error still exists, it immediately re-adds the error to the page on remove. Removing alertContext from the array works, but it is not a real fix and linter yells.
useEffect(() => {
if (query.isError) {
alertContext.add(query.error.toString())
}
}, [alertContext, query.isError, query.error])
This is a perfectly fine approach to the problem. You've also accurately identified the problem. The solution is to create a second hook with access to the methods that will modify the context. AppAlert needs access to the data in the context, and needs to update when AlertContext.alert changes. UseAccess only needs to be able to call AlertContext.add, and that method wont change and trigger a re-render. This can be done with a second Context. You can just expose one Provider and bake the actions provider into the outer context provider.
import React, { useState, createContext, useContext } from 'react'
const AlertContext = createContext()
const AlertContextActions = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = () => setAlert(null)
const addAlert = (message) => setAlert(message)
return (
<AlertContext.Provider value={{ alert }}>
<AlertContextActions.Provider value={{ addAlert, removeAlert }}>
{children}
</AlertContextActions.Provider>
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
Now, where you need access to the alert you use one hook and where you need access to the actions you use the other.
// in AppAlert
import { useAlertContext, useAlertContextActions } from '#/context/alert-context/alert-context'
...
const { alert } = useAlertContext()
const { removeAlert } = useAlertContextActions()
And finally
// in useAccess
import { useAlertContextActions } from '#/context/alert-context/alert-context'
...
const { addAlert } = useAlertContextActions()
So I found a solution that seems to work for my purposes. I got a hint from this article. https://mortenbarklund.com/blog/react-architecture-provider-pattern/
Note the use of useCallback above. It ensures minimal re-renders of components using this context, as the function is guaranteed to be stable (as its memoized without dependencies).
So with this I tried the following and it solved the problem.
import React, { useState, createContext, useContext, useCallback } from 'react'
const AlertContext = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = useCallback(() => setAlert(null), [])
const addAlert = useCallback((message) => setAlert(message), [])
return (
<AlertContext.Provider value={{
alert,
add: addAlert,
remove: removeAlert
}}
>
{children}
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
My goal: Global error handling
One problem with the above useEffect approach is that every invocation of useAccess will run their own effects. So if you have useAccess twice on the page, and it fails, you will get two alerts, so it's not really "global".
I would encourage you to look into the global callbacks on the QueryCache in react-query. They are made for this exact use-case: To globally handle errors. Note that to use context, you would need to create the queryClient inside the Application, and make it "stable" with either useRef or useState:
function App() {
const alertContext = useAlertContext()
const [queryClient] = React.useState(() => new QueryClient({
queryCache: new QueryCache({
onError: (error) =>
alertContext.add(error.toString())
}),
}))
return (
<QueryClientProvider client={queryClient}>
<RestOfMyApp />
</QueryClientProvider>
)
}
I also have some examples in my blog.
Im building a react app that fetches a random food data from spoonacular.com. Im trying to display the title name of the food on the page but it doesn't show up and also why does it keep fetching a bunch of different data as shown in the picture of the console.log even though I specified the number of data to fetch as 1 in the URL
This is my Home.js
import React, { useEffect, useState } from "react";
import axios from "axios";
import Recipe from "../components/Recipes";
const URL = `https://api.spoonacular.com/recipes/random?apiKey=${APIKey}&number=1`;
function Home() {
const [food, setFood] = useState({});
useEffect(() => {
axios
.get(URL)
.then(function (response) {
setFood(response.data);
console.log(food);
})
.catch(function (error) {
console.warn(error);
});
}, [food]);
return (
<main>
<Recipe recipeList={food} />
</main>
);
}
export default Home;
and this is my Recipe.js component
import React from "react";
function Recipe({ recipeList }) {
return (
<div className="recipeCard">
<h1>{recipeList.title}</h1>
</div>
);
}
export default Recipe;
and this is the picture of the console when I log the results fetched from the API (they're all different food datas but I only wanted to fetch 1 food data and display it on the page)
That's right, you get 1 random recipe, but useEffect works every time you update the food state, so you have an infinite loop. Just remove food from useEffect dependency. It's also better to check if recipeList exists so you don't get a missing title error
This should work as expected:
Home.js:
import React, { useEffect, useState } from "react";
import axios from "axios";
import Recipe from "../components/Recipes";
const URL = `https://api.spoonacular.com/recipes/random?apiKey=${APIKey}&number=1`;
function Home() {
const [food, setFood] = useState(null);
useEffect(() => {
axios
.get(URL)
.then(function (response) {
setFood(response.data);
console.log(food);
})
.catch(function (error) {
console.warn(error);
});
}, []);
return (
<main>
<Recipe recipeList={food} />
</main>
);
}
export default Home;
Recipe.js:
import React from "react";
function Recipe({ recipeList }) {
if(!recipeList) return <></>
return (
<div className="recipeCard">
<h1>{recipeList?.title}</h1>
</div>
);
}
export default Recipe;
I have read questions with similar titles and they have not solved my problem.
I have an API call whose result needs to be shared amongst several components. The parent component makes the call and React's context is used to share it amongst the child components:
MainPage.js:
import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { VideoPlayer } from "../components/VideoPlayer";
import VideoContext from "../components/VideoContext";
export default function Watch() {
const [video, setVideo] = useState({});
const { videoHash } = useParams();
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsLoading(true);
getVideo();
}, [videoHash]);
const getVideo = async () => {
if(videoHash) {
const res = await getVideoFromApi();
setIsLoading(false);
// The first time this runs nothing happens, the video can't be played
// The second time it runs (i.e. when the URL/videoHash changes) it updates
// but it shows the video from the first run
setVideo(res.video);
}
};
return (
<>
{isLoading ? (
<div>Loading...</div>
) : (
<VideoContext.Provider value={{ video, setVideo }}>
<VideoPlayer videoHash={videoHash} />
</VideoContext.Provider>
)}
</>
);
}
VideoPlayer.js:
import React, { useState, useEffect, useContext } from "react";
import VideoContext from "./VideoContext";
import styles from "./VideoPlayer.module.css";
export function VideoPlayer({ videoHash }) {
const { video, setVideo } = useContext(VideoContext);
const [forceRender, setforceRender] = useState(true);
useEffect(() => {
// I tried adding this to no effect
setforceRender(!forceRender);
}, [videoHash]);
return (
<video controls className={styles["video-player"]}>
<source src={video.VideoUrl} type="video/mp4" />
Sorry, your browser does not support embedded videos.
</video>
);
}
VideoContext.js:
import { createContext } from "react";
export default createContext({
video: {},
setVideo: () => {}
});
It works when the page loads, but when my Link components change the videoHash property the new video loads (I can see when I console.log() the API call) but it does not update in the video player.
The second time a link is clicked and the videoHash param is changed, the video displays but it's for the previous video.
https://codesandbox.io/s/blazing-lake-k4i8n?file=/src/VideoPlayer.js
Unless I'm missing something, I think the VideoPlayer would be all right just behaving as a functional component without any hooks for state, that could be handled by Watch. When you click on a link to another route that will point to watch, the videoHash will change
VideoPlayer.js
import React from "react";
import { Link } from "react-router-dom";
export function VideoPlayer({ videoHash }) {
// const { video, setVideo } = useContext(VideoContext);
// const [forceRender, setforceRender] = useState(true);
// useEffect(() => {
// // I tried adding this to no effect
// setforceRender(!forceRender);
// }, [videoHash]);
// console.log(video);
// Am I missi
return (
<div>
Am I missing something or could you just use your videoHash: {videoHash},
here?
<Link to="/watch/a">Previous</Link>
<Link to="/watch/c">Next</Link>
</div>
);
}
Watch.js
import React, { useState, useEffect, useCallback } from "react";
import { useParams } from "react-router-dom";
import { VideoPlayer } from "./VideoPlayer";
import VideoContext from "./VideoContext";
export default function Watch() {
const [video, setVideo] = useState({});
const { videoHash } = useParams();
const [isLoading, setIsLoading] = useState(true);
const getVideo = useCallback(async () => {
if (videoHash) {
const res = await getVideoFromApi();
setTimeout(() => {
setIsLoading(false);
setVideo(res);
}, 1000);
}
}, [videoHash]);
useEffect(() => {
setIsLoading(true);
getVideo();
}, [getVideo]);
const getVideoFromApi = async () => {
const videoArray = ["A", "B", "C"];
const randomItem =
videoArray[Math.floor(Math.random() * videoArray.length)];
return Promise.resolve(randomItem);
};
return (
<>
{isLoading ? (
<div>Loading...</div>
) : (
<VideoContext.Provider value={{ video, setVideo }}>
<VideoPlayer videoHash={videoHash} />
</VideoContext.Provider>
)}
</>
);
}
VideoContext.js
import { createContext } from "react";
export default createContext({
video: "",
setVideo: () => {}
});
I added a timeout so you can see the loading part work as well. Let me know if there's something I'm missing about what you need to do.
Here is how I have created a Context for simple program I am writing
import React, { useState, createContext, useEffect } from "react";
export const PhotoContext = createContext();
export const PhotoProvider = (props) => {
const [photo, setPhoto] = useState([]);
useEffect(() => {
console.log("Use Effect Runs HAHAHAH");
console.log("HAHAHAHAHAHAHAH");
fetchPhotos();
async function fetchPhotos() {
const url =
"https://raw.githubusercontent.com/bobziroll/scrimba-react-bootcamp-images/master/images.json";
fetch(url)
.then((res) => res.json())
.then((arr) => {
setPhoto(arr);
})
.catch(console.log("ERROR"));
}
}, []);
return (
<PhotoContext.Provider value={[photo, setPhoto]}>
{props.children}
</PhotoContext.Provider>
);
};
There is another file where I want to load the data in the photos variable. Here is the code for it. I have used setTimeout to see where exactly is the problem. It seems whenever the statement in setTimeout runs, the value in console in returned twice. First, it is empty and the second has the actual value. But since, I try to access the photos.url, and since the first time it is undefined, the program collapses.
import React, { useState, useContext } from "react";
import { PhotoContext } from "../Context/PhotoContext";
const Photos = (props) => {
const [photos, values] = useContext(PhotoContext);
setTimeout(() => {
console.log(photos[0].url);
}, 3000);
return <div>{}</div>;
};
export default Photos;
Help would be really appreciated.
Didn't see the problem. I created sandbox for your example.
https://codesandbox.io/s/inspiring-lovelace-5r1gb?file=/src/App.js