I'm trying to set components with 3 functionalities. Displaying PokemonList, getting random pokemon and find one by filters. Getting random pokemon works great but since 2 days I'm trying to figure out how to set pokemon list feature correctly
Below full code from this component.
It's render when click PokemonsList button inside separate navigation component and fire handleGetPokemonList function in provider using context.
The problem is that I can't manage rerender components when PokemonList is ready. For now i need to additionally fire forceUpadte() function manually (button onClick = () => forceUpdate())
I tried to use useEffect() in PokemonList component but it didn't work in any way.
I was also sure that after fetching data with fetchData() function I can do .then(changeState of loading) but it didn't work also.
What Am I missing to automatically render data from fetch in provider in PokemonList component? I'm receiving error about no data exist but if I use forceUpdate then everything is ok
Complete repo here: https://github.com/Mankowski92/poke-trainer
handleGetPokemonList function in provider below
const handleGetPokemonList = () => {
setCurrentPokedexOption('pokemonList');
async function fetchData() {
setImgLoaded(false);
let res = await fetch(`${API}?offset=0&limit=6/`);
let response = await res.json();
response.results.forEach((item) => {
const fetchDeeper = async () => {
let res = await fetch(`${item.url}`);
let response = await res.json();
let eachPoke = {
id: response.id,
name: response.name,
artwork: response.sprites.other['officialartwork'].front_default,
stats: response.stats,
};
fetchedPokemons.push(eachPoke);
};
fetchDeeper();
});
setPokemonList(fetchedPokemons);
if (fetchedPokemons) {
return setLoading(false);
}
}
fetchData()
.then((res) => setLoading(res))
.catch((err) => console.log('error', err));
};
PokemonList component below
import React, { useContext, useState, useCallback } from 'react';
import { StyledPokemonListContainer } from './PokemonList.styles';
import { PokemonsContext } from '../../../providers/PokemonsProvider';
const PokemonList = () => {
const ctx = useContext(PokemonsContext);
const [, updateState] = useState();
const forceUpdate = useCallback(() => updateState({}), []);
const { handleSetImgLoaded } = useContext(PokemonsContext);
return (
<>
{ctx.currentPokedexOption === 'pokemonList' ? (
<StyledPokemonListContainer>
{ctx.pokemonList && ctx.pokemonList.length ? (
ctx.pokemonList.map((item, i) => (
<div className="each-pokemon-container" key={i}>
<div className="poke-id">{item.id}</div>
<div className="poke-name">{item.name}</div>
<img className="poke-photo" onLoad={() => handleSetImgLoaded()} src={item ? item.artwork : ''} alt="" />
</div>
))
) : (
<div className="render-info">Hit rerender button</div>
)}
{/* {ctx.pokemonList ? <div>{ctx.pokemonList[0].name}</div> : <div>DUPPSKO</div>} */}
<div className="buttons">
<button onClick={() => console.log('PREVOIUS')}>Previous</button>
<button className="rerender-button" onClick={() => forceUpdate()}>
RERENDER
</button>
<button onClick={() => console.log('NEXT')}>Next</button>
</div>
</StyledPokemonListContainer>
) : null}
</>
);
};
export default PokemonList;
Related
Can anyone please explain me what const rerender = React.useState(0)[1] is this?
import React from 'react'
import axios from 'axios'
import {
useQuery,
useQueryClient,
QueryClient,
QueryClientProvider,
} from "#tanstack/react-query"
import { ReactQueryDevtools } from "#tanstack/react-query-devtools"
const getCharacters = async () => {
await new Promise((r) => setTimeout(r, 500))
const { data } = await axios.get('https://rickandmortyapi.com/api/character/')
return data
}
const getCharacter = async (selectedChar) => {
await new Promise((r) => setTimeout(r, 500))
const { data } = await axios.get(
`https://rickandmortyapi.com/api/character/${selectedChar}`,
)
return data
}
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
function Example() {
const queryClient = useQueryClient()
**const rerender = React.useState(0)[1]**
const [selectedChar, setSelectedChar] = React.useState(1)
const charactersQuery = useQuery(['characters'], getCharacters)
const characterQuery = useQuery(['character', selectedChar], () =>
getCharacter(selectedChar),
)
return (
<div className="App">
<p>
Hovering over a character will prefetch it, and when it's been
prefetched it will turn <strong>bold</strong>. Clicking on a prefetched
character will show their stats below immediately.
</p>
<h2>Characters</h2>
{charactersQuery.isLoading ? (
'Loading...'
) : (
<>
<ul>
{charactersQuery.data?.results.map((char) => (
<li
key={char.id}
onClick={() => {
setSelectedChar(char.id)
}}
onMouseEnter={async () => {
await queryClient.prefetchQuery(
['character', char.id],
() => getCharacter(char.id),
{
staleTime: 10 * 1000, // only prefetch if older than 10 seconds
},
)
setTimeout(() => {
**rerender({})**
}, 1)
}}
>
<div
style={
queryClient.getQueryData(['character', char.id])
? {
fontWeight: 'bold',
}
: {}
}
>
{char.id} - {char.name}
</div>
</li>
))}
</ul>
<h3>Selected Character</h3>
{characterQuery.isLoading ? (
'Loading...'
) : (
<>
<pre>{JSON.stringify(characterQuery.data, null, 2)}</pre>
</>
)}
<ReactQueryDevtools initialIsOpen />
</>
)}
</div>
)
}
I want to know what it means, i am unable to understand this useState syntax I never seen this type of syntax. Can anyone share something about this?
useState has two parts the value and a function to update the value.
Take a look at the below snippet which explains how the useState hook is assigning values normally.
var fruitStateVariable = useState('banana'); // Returns a pair
var fruit = fruitStateVariable[0]; // The value of the state
var setFruit = fruitStateVariable[1]; // A asynchronous function to update the state.
By accessing the item with index 1 you are assigning rerender a function to update the state, which will trigger a rerender, as React sees the value going from 0 -> {}.
As to why the code is doing this, it seems the author is trying to get around useState being asynchronous to update.
This is not a good pattern and should be avoided!. As you rightly have said you have not seen this syntax before, because it is not an appropriate way of triggering a function to rerender.
React Docs recommends this way if you really need to force the retrigger:
const forceUpdate = useReducer(x => x + 1, 0)[1]
But a key line from this resource is to Try to avoid this pattern if possible.
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
import { useSelector, useDispatch } from "react-redux";
import { useEffect, useState } from "react";
import { useHistory } from "react-router-dom";
import { IoMdArrowRoundBack } from "react-icons/io";
import axios from "axios";
import { fetchMovies } from "../../feautures/movies/moviesSlice";
import Rating from "../../components/UI/Rating/Rating";
import request from "../../requests";
import "./SingleMoviePage.scss";
import SimilarMovies from "../../components/SimilarMovies/SimilarMovies";
const SingleMoviePage = ({ match }) => {
const dispatch = useDispatch();
const [movieDetails, setMovieDetails] = useState({});
const [movieCredits, setMovieCredits] = useState({});
const history = useHistory();
console.log("single rendered")
// number month to string
const date = new Date(movieDetails.release_date);
const dateWithMonthName =
date.getFullYear() +
"-" +
date.toLocaleString("en-EN", { month: "long" }) +
"-" +
date.getDay();
/* params */
const movieId = match.params.id;
const page = match.params.page;
const genre = match.params.genre;
/* movies reducer handle */
const moviesStatus = useSelector((state) => state.movies.status);
/* base urls */
const baseImgUrl = "https://image.tmdb.org/t/p/original";
const movieDetailUrl = `https://api.themoviedb.org/3/movie/${movieId}?api_key=c057c067b76238e7a64d3ba8de37076e&language=en-US`;
const movieCastUrl = `https://api.themoviedb.org/3/movie/${movieId}/credits?api_key=c057c067b76238e7a64d3ba8de37076e&language=en-US`;
// go home page
const goHOme = () => {
history.goBack()
};
// fetch movie cast
useEffect(() => {
const fetchMovieCast = async () => {
let response = await axios.get(movieCastUrl);
response = response.data;
setMovieCredits(response);
};
fetchMovieCast();
}, [movieCastUrl]);
// fetch movie details
useEffect(() => {
const fetchMovieDetails = async () => {
let response = await axios.get(movieDetailUrl);
response = response.data;
setMovieDetails(response);
};
fetchMovieDetails();
}, [movieDetailUrl]);
let content;
if (moviesStatus === "loading") {
} else if (moviesStatus === "succeeded") {
content = (
<div
className="single-movie__container"
style={{
backgroundImage: `url(${
movieDetails.backdrop_path
? baseImgUrl + movieDetails.backdrop_path
: baseImgUrl + movieDetails.poster_path
})`,
}}
>
<div className="single-movie__details">
<IoMdArrowRoundBack
className="single-movie__back"
onClick={goHOme}
size={65}
color={"#e50914"}
/>
<h1 className="single-movie__title">{movieDetails.title}</h1>
<div className="single-movie__rate">
<Rating
rating={movieDetails.vote_average}
className="single-movie__stars"
/>
</div>
<p className="single-movie__overview">{movieDetails.overview}</p>
<div className="single-movie__informations single-movie__informations--genres">
<label className="single-movie__informations-heading">Genres</label>
<div className="single-movie__informations-container">
{movieDetails.genres?.map((genre) => {
return <div className="single-movie__info">{genre.name}</div>;
})}
</div>
</div>
<div className="single-movie__informations single-movie__informations--stars">
<label className="single-movie__informations-heading">
Starring
</label>
<div className="single-movie__informations-container">
{movieCredits.cast?.slice(0, 4).map((star) => {
return <div className="single-movie__info">{star.name}</div>;
})}
</div>
</div>
<div className="single-movie__informations single-movie__informations--released">
<label className="single-movie__informations-heading">
Release Date
</label>
<div className="single-movie__informations-container">
<div className="single-movie__info">{dateWithMonthName}</div>
</div>
</div>
<div className="single-movie__informations single-movie__informations--production">
<label className="single-movie__informations-heading">
Production
</label>
<div className="single-movie__informations-container">
{movieDetails.production_countries?.slice(0, 2).map((country) => {
return <div className="single-movie__info">{country.name}</div>;
})}
</div>
</div>
</div>
<SimilarMovies movieId={movieId} />
</div>
);
}
useEffect(() => {
if (genre === "POPULAR") {
dispatch(fetchMovies(request.fetchPopular(page)));
} else if (genre === "NOW PLAYING") {
dispatch(fetchMovies(request.fetchNowPlaying(page)));
} else if (genre === "UP COMING") {
dispatch(fetchMovies(request.fetchUpComing(page)));
}
}, [dispatch, genre, page]);
return <div className="single-movie">{content}</div>;
};
export default SingleMoviePage;
Hi all.When i clicked Card compenent it navigate me to the SingleMoviePage component.But SingleMoviePage component re-render five times.How can i find this issues's source ? And how can i prevent that ? Finally is there any problem to fetch MovieCast and MovieDetails in same useEffect hook ?
github repo : https://github.com/UmutPalabiyik/hope-movie-app
demo : https://hope-movie.web.app/page/1
The first 2 useEffect hooks fetch data separately which then update your local states, which then triggers re-rendering.
If you don't want to re-render after each data fetch (state update), I'd suggest adding a loading state. Set it to true first and return a loading icon as your content. Then after both movieDetails and movieCredits have been populated, set it to false and return the actual content. This should render twice in total.
Have you considered graphql? GraphQL can combine your api calls into one and it also handles loading state and error state.
Whatever solution you have, re-rendering will happen when you are fetching data. Initial render won't have any data and after fetching data it must re-render.
You should use only one useEffect hook your code is running for all three. React will handle the rest itself.
I'm working on a component that adds images to items. You can either upload your own image or pick an image, loaded from an API based on the name of the item.
Here is the root component:
const AddMedia = (props) => {
const [currentTab, setCurrentTab] = useState(0);
const [itemName, setItemName] = useState(props.itemName);
return (
<div>
<Tabs
value={currentTab}
onChange={() => setCurrentTab(currentTab === 0 ? 1 : 0)}
/>
<div hidden={currentTab !== 0}>
<FileUpload />
</div>
<div hidden={currentTab !== 1}>
{currentTab === 1 && <ImagePicker searchTerm={itemName} />}
</div>
</div>
);
};
And here is the <ImagePicker />:
import React, { useState, useEffect } from "react";
function ImagePicker({ searchTerm, ...props }) {
const [photos, setPhotos] = useState([]);
const searchForImages = async (keyword) => {
const images = await api.GetImagesByKeyword(keyword);
return images;
};
useEffect(() => {
const result = searchForImages(searchTerm);
setPhotos(result);
}, []);
return (
<>
{photos.map(({ urls: { small } }, j) => (
<img alt={j} src={small} className={classes.img} />
))}
</>
);
}
const areSearchTermsEqual = (prev, next) => {
return prev.searchTerm === next.searchTerm;
};
const MemorizedImagePicker = React.memo(ImagePicker, areSearchTermsEqual);
export default MemorizedImagePicker;
What I'm struggling with is getting the component to not fetch the results again if the searchTerm hasn't changed. For example, when the component loads it's on tab 0 (upload image), you switch to tab 1 (pick an image) and it fetches the results for searchTerm, then you switch to 0 and again to 1 and it fetches them again, although the searchTerm hasn't changed. As you can see, I tried using React.memo but to no avail. Also, I added the currentTab === 1 to stop it from fetching the photos when the root component renders and fetch them only if the active tab is 1.
You should add the searchTerm as dependency of the useEffect so that it will not fetch again if searchTerm hasn't change:
useEffect(() => {
const result = searchForImages(searchTerm);
setPhotos(result);
}, [searchTerm]);
Additional information, if you are using eslint to lint your code, you can use the react-hooks/exhaustive-deps rule to avoid this kind of mistake.
I stuck in this moment creating store with different products, that I want to add to the basket. The problem occur when I wanted to pass the state of cardList into Basket component to change the information from "Basket is empty" to display information how many items are currently in basket.
Below I paste my main hooks component with basket component which include all functionality.
Basket component:
import React from 'react'
const Basket = (props) => {
return (
<div>
{props.cardItems.length === 0 ? "Basket is empty" : <div> You have {props.cardItems.length} products in basket!</div>}
</div>
)
}
export default Basket;
Main component:
function
const [cardItems, setCardItems] = useState([]);
const price = 2.50;
useEffect(() => {
fetch(URL, {
method: 'GET',
headers: {
Accept: "application/json",
}
}).then(res => res.json())
.then(json => (setBeers(json), setFilteredBeers(json))
);
}, [])
function handleMatches(toMatch) {...
}
const displayFilterBeers = event => {...
}
const handleRemoveCard = () => {...
}
const handleAddToBasket = (event, beer) => {
setCardItems(state => {
let beerAlreadyInBasket = false;
cardItems.forEach(item => {
if (item.id === beer.id) {
beerAlreadyInBasket = true;
item.count++;
};
});
if (!beerAlreadyInBasket) {
cardItems.push({ ...beer, count: 1 })
}
localStorage.setItem('baketItems', JSON.stringify(cardItems));
console.log('cardItems: ', cardItems, cardItems.length);
return cardItems;
})
}
return (
<div className="App">
<div className='search'>
<input type='text' placeholder='search beer...' onChange={displayFilterBeers} />
</div>
<BeerList BeersList={filteredBeers} price={price} handleAddToBasket={handleAddToBasket} />
<Basket cardItems={cardItems}/>
</div>
);
}
export default App;
I saw an example that without React hooks that in basket component someone used const {cartItems} = this.props; but I don't know how to achieve something similar using hooks.
I think what you are facing is related to this issue.
So when dealing with array or list as state, react doesn't re-render if you don't set state value to a new instance. It assumes from the high-level comparison that the state hasn't been changed. So it bails out from the re-rendering.
from the issue I found this solution is better than the others -
const handleAddToBasket = (event, beer) => {
const nextState = [...cardItems, beer] // this will create a new array, thus will ensure a re-render
// do other stuffs
setCardItems(nextState);
};