Fairly new to react and trying to build a clone of The Movie Database site. I want this toggle switch to change my api call from movies to tv. It starts working after a couple clicks, but then it throws everything off and it's not displaying the correct items anyway. Not really sure what's going on here...or even why it starts working after two clicks. Anyone know whats up with this?
import React, { useState, useEffect } from "react";
import axios from "axios";
import API_KEY from "../../config";
const Popular = ({ imageUri }) => {
// GET POPULAR MOVIES
const [popularMovies, setPopularMovies] = useState("");
const [genre, setGenre] = useState("movie");
console.log(genre);
const getPopular = async () => {
const response = await axios.get(
`https://api.themoviedb.org/3/discover/${genre}?sort_by=popularity.desc&api_key=${API_KEY}`
);
setPopularMovies(response.data.results);
};
useEffect(() => {
getPopular();
}, []);
const listOptions = document.querySelectorAll(".switch--option");
const background = document.querySelector(".background");
const changeOption = (el) => {
let getGenre = el.target.dataset.genre;
setGenre(getGenre);
getPopular();
listOptions.forEach((option) => {
option.classList.remove("selected");
});
el = el.target.parentElement.parentElement;
let getStartingLeft = Math.floor(
listOptions[0].getBoundingClientRect().left
);
let getLeft = Math.floor(el.getBoundingClientRect().left);
let getWidth = Math.floor(el.getBoundingClientRect().width);
let leftPos = getLeft - getStartingLeft;
background.setAttribute(
"style",
`left: ${leftPos}px; width: ${getWidth}px`
);
el.classList.add("selected");
};
return (
<section className="container movie-list">
<div className="flex">
<div className="movie-list__header">
<h3>What's Popular</h3>
</div>
<div className="switch flex">
<div className="switch--option selected">
<h3>
<a
data-genre="movie"
onClick={(e) => changeOption(e)}
className="switch--anchor"
>
In Theaters
</a>
</h3>
<div className="background"></div>
</div>
<div className="switch--option">
<h3>
<a
data-genre="tv"
onClick={(e) => changeOption(e)}
className="switch--anchor"
>
On TV
</a>
</h3>
</div>
</div>
</div>
<div className="scroller">
<div className="flex flex--justify-center">
<div className="flex flex--nowrap container u-overScroll">
{popularMovies &&
popularMovies.map((movie, idX) => (
<div key={idX} className="card">
<div className="image">
<img src={imageUri + "w500" + movie.poster_path} />
</div>
<p>{movie.title}</p>
</div>
))}
</div>
</div>
</div>
</section>
);
};
export default Popular;
You're using the array index as your key prop when you're mapping your array.
You should use an id that is specific to the data that you're rendering.
React uses the key prop to know which items have changed since the last render.
In your case you should use the movie id in your key prop instead of the array index.
popularMovies.map((movie) => (
<div key={movie.id} className="card">
<div className="image">
<img src={imageUri + 'w500' + movie.poster_path} />
</div>
<p>{movie.title}</p>
</div>
));
Also
You're calling the api directly after setGenre. However state changes aren't immediate. So when you're making your api call you're still sending the last movie genre.
Two ways of fixing this:
You could call your function with the genre directly, and change your function so it handles this case:
getPopular('movie');
Or you could not call the function at all and add genre as a dependency of your useEffect. That way the useEffect will run each time the genre change.
useEffect(() => {
getPopular();
}, [genre]);
PS: You should consider splitting your code into more component and not interacting with the DOM directly.
To give you an idea of what it could look like, I refactored a bit, but more improvements could be made:
const Popular = ({ imageUri }) => {
const [popularMovies, setPopularMovies] = useState('');
const [genre, setGenre] = useState('movie');
const getPopular = async (movieGenre) => {
const response = await axios.get(
`https://api.themoviedb.org/3/discover/${movieGenre}?sort_by=popularity.desc&api_key=${API_KEY}`
);
setPopularMovies(response.data.results);
};
useEffect(() => {
getPopular();
}, [genre]);
const changeHandler = (el) => {
let getGenre = el.target.dataset.genre;
setGenre(getGenre);
};
const isMovieSelected = genre === 'movie';
const isTvSelected = genre === 'tv';
return (
<section className="container movie-list">
<div className="flex">
<MovieHeader>What's Popular</MovieHeader>
<div className="switch flex">
<Toggle onChange={changeHandler} selected={isMovieSelected}>
In Theaters
</Toggle>
<Toggle onChange={changeHandler} selected={isTvSelected}>
On TV
</Toggle>
</div>
</div>
<div className="scroller">
<div className="flex flex--justify-center">
<div className="flex flex--nowrap container u-overScroll">
{popularMovies.map((movie) => {
const { title, id, poster_path } = movie;
return (
<MovieItem
title={title}
imageUri={imageUri}
key={id}
poster_path={poster_path}
/>
);
})}
</div>
</div>
</div>
</section>
);
};
export default Popular;
const Toggle = (props) => {
const { children, onChange, selected } = props;
const className = selected ? 'switch--option selected' : 'switch--option';
return (
<div className={className}>
<h3>
<a
data-genre="movie"
onClick={onChange}
className="switch--anchor"
>
{children}
</a>
</h3>
<div className="background"></div>
</div>
);
};
const MovieHeader = (props) => {
const { children } = props;
return (
<div className="movie-list__header">
<h3>{children}</h3>
</div>
);
};
const MovieItem = (props) => {
const { title, imageUri, poster_path } = props;
return (
<div key={idX} className="card">
<div className="image">
<img src={imageUri + 'w500' + poster_path} />
</div>
<p>{title}</p>
</div>
);
};
Related
The first problem is when I use:
const [visible, setVisible] = useState(4);
and put it in:
products.slice(0, visible)
It is showing me all the products rather than 4. This issue doesn't occur when I use it like this:
products.slice(0, 4)
The second problem is that I want my button "Load More" to increase value of Visible but it is not working.
Here is the complete code:
import React, {useEffect, useState} from 'react'
import './ForYou.css'
import ForYouItem from './ForYouItem'
export default function ForYou(props) {
const [products, setProducts] = useState([]);
const [visible, setVisible] = useState(4);
useEffect(() => {
fetch('https://fakestoreapi.com/products')
.then((res) => res.json())
.then((data) => setProducts(data))
}, [])
const showMoreItems = () => {
setVisible((prevValue) => prevValue + 4)
}
return (
<div>
<div className="ForYou-container">
<div className="heading">
<a href="#" className='Big-text'> {props.Bigheading}</a>
</div>
<div className="row ">
{products.slice(0, visible).map((product) => {
return(
<div className="col-md-3 my-2 Products">
<ForYouItem Title={product.title.slice(0,50)} Price={product.price} Imageurl = {product.image}/>
</div>
)
}
)}
<button className='Load-btn' onClick={showMoreItems}>Load More</button>
</div>
</div>
</div>
)
}
Code for child component:
import React from 'react'
import './ForYouItem.css'
export default function ForYouItem(props) {
return (
<div>
<a href="#">
<div class="card" >
<img src={props.Imageurl} class="card-img-top" alt="..."/>
<div class="card-body">
<h5 class="card-title"> {props.Title}... </h5>
<p class="card-text">Rs.{props.Price}</p>
Buy Now!
</div>
</div>
</a>
</div>
)
}
I have tried your code and make a few modifications since there was no code for the child component so I can make it work. However I did not find any errors, check this code to see if you get any ideas on how to implement the increasing of the items and check the ForYouItems component as there might be the issue since it renders ok with less data.
import React, {useEffect, useState} from 'react'
const ForYouItem = ({Title})=>{
return <>
<div>{Title}</div>
</>
}
export default function ForYou(props) {
const [products, setProducts] = useState([]);
const [visible, setVisible] = useState(4);
useEffect(() => {
fetch('https://fakestoreapi.com/products')
.then((res) => res.json())
.then((data) => setProducts(data))
}, [])
const showMoreItems = () => {
setVisible((prevValue) => prevValue + 4)
}
return (
<div>
<div className="ForYou-container">
<div className="heading">
<a href="#" className='Big-text'>props.Bigheading</a>
</div>
<div className="row ">
{products.slice(0, visible).map((product) => {
return(
<div className="col-md-3 my-2 Products">
<ForYouItem Title={product.title.slice(0,50)} />
</div>
)
}
)}
{
visible <= products.length-1 &&
<button className='Load-btn' onClick={showMoreItems}>Load More</button>
}
</div>
</div>
</div>
)
}
Updated code: Im trying to first display carsList and only when selectedMake is selected, I would update the state with the result from filter and show another array. I tried storing carsList in updatedCarsList so it has all the cars on page load but Im missing something here.
CarOffers.jsx
const CarOffers = () => {
const [carsList, setCarsList] = useState([]);
const [updatedCarsList, setUpdatedCarsList] = useState([]);
const [selectedMake, setSelectedMake] = useState(undefined);
const getCars = () => {
axios.get(url)
.then((response) => {
return setCarsList(response.data)
})
}
const handleMakeChange = (select) => {
setSelectedMake(select.value)
}
const applyFilters = () => {
let updatedCarsList = carsList
if(selectedMake) {
updatedCarsList = carsList.filter(car => car.make === selectedMake)
setUpdatedCarsList(updatedCarsList);
} else {
setUpdatedCarsList(carsList)
}
}
useEffect(() => {
getCars()
applyFilters()
}, [ selectedMake ]);
return (
<div className="mka__wrapper-car-offers">
<div className="mka__container">
<div className="mka__content-car-offers">
<div className="mka__content-grid-offers">
<div className="item1">
< CarSlider/>
<div className="mka-responsive-item">
< DisplayCars/>
< SortingCars/>
< CarAlignment/>
</div>
</div>
<div className="item2">
<div className="mka__side-bar-divider">
< Search
carsList={carsList}/>
</div>
<div>
< FilterSideBar
carsList={carsList}
handleMakeChange={handleMakeChange} />
</div>
</div>
<div className="item3">
<Cars updatedCarsList={updatedCarsList}/>
</div>
</div>
</div>
</div>
</div>
)
}
export default CarOffers;
Cars.jsx
const Cars = ({ updatedCarsList }) => {
return (
<div className='mka__cars-grid'>
{updatedCarsList.map(car =>
<CarsItem key={car.id} car={car}/>)}
</div>
)
}
export default Cars
CarItem.jsx
const CarsItem = ({car: {year,month,transmission,mileage,price,title,link}}) => {
return (
<Fragment>
<div className="cars-item_wrapper">
<div className="cars-item_image">
<img src={link} alt="car" />
</div>
<div>
<a
className="cars-item_car-title"
href="/"
>
{title}
</a>
</div>
<div className=" cars-item_separator"></div>
<p className="cars-item_car-text">{price}</p>
</div>
</Fragment>
)
}
export default CarsItem
Move your applyFilters above getCars
Does Select need to be in <>
distinctBy... urgh.. use Set const unique = [...new Set(data.map(item => item.value))]
applyFilters... axios is async, but your setting a value so state doesn't update so no re-render? Maybe.
selectedMake - don't use null as a default, use undefined.
Hope that helps, feels like a state management issue.
... think its this ....
You are using carsList as your list of cars, however you are setting the value of carsList with setCarsList(updatedCarsList)... updatedCarsList is a filtered list of cars... only car => car.make === selectedMake so once you've selected a make your carList is only cars with the selected make.
Solution is to
Either separate the list from the filtered list
or preferably keep list, but pass the filtered state to the component that needs it... but not update state of the original list by calling setCarsList(updatedCarsList);
if (selectedMake){
updatedCarsList = updatedCarsList.filter(
car => car.make === selectedMake
)
};
setCarsList(updatedCarsList);
I'm trying to change the state of a component with data from an external api. The state is to add a component as a favourite, but returns undefined. I'm already using arrow functions and the .bind() didn't work as well. What am I missing here?
Library Component:
export default function LibraryComponent() {
const [albums, setAlbums] = useState<any[]>([]);
const [albumTitle, setAlbumTitle] = useState<any[]>([]);
const [photoUrl, setPhotoUrl] = useState();
const [favourite, setFavourite] = useState<any[]>([]);
//FETCH DATA
const fetchData = () => {
const getAlbums = "https://jsonplaceholder.typicode.com/albums?_limit=20";
const getPhotos =
"https://627ed423b75a25d3f3bd811f.mockapi.io/api/photos/1"; //random number here
const albums = axios.get(getAlbums).then((res) => {
setAlbums(res.data);
});
const photoUrl = axios.get(getPhotos).then((res) => {
setPhotoUrl(res.data.images);
});
};
const addFavourite = (album: []) => {
const favouriteList = [...favourite, album];
setFavourite(favouriteList);
};
return (
<>
<div className="container-fluid w-50">
<div className="row">
{albums.map((album) => (
<div key={album.id} className="col-lg-2 col-sm-6">
<div className="col-lg-12 col-sm-6">
<div className="thumbnail img-responsive">
<AlbumComponent
title={album.title}
image={photoUrl}
handleFavouriteClick={addFavourite}
/>
</div>
</div>
</div>
))}
</div>
</div>
</>
);
}
Album Component:
type Meta = {
title: any;
image: any;
handleFavouriteClick: any;
};
export default function AlbumComponent(props: Meta) {
return (
<>
<img src={props.image} alt="" className="img-fluid img-thumbnail" />
<p>{props.title}</p>
<div onClick={() => props.handleFavouriteClick()}>
<i className="fa-regular fa-heart"></i>
</div>
</>
);
}
addFavourite expects an album parameter.
Try something like this:
<AlbumComponent
title={album.title}
image={photoUrl}
handleFavouriteClick={() => addFavourite(album)}
/>
You need to pull the value from the input and provide it as a parameter to the function. You could use a controlled input for this. Somehting like this:
export default function AlbumComponent(props: Meta) {
const [value, setValue] = React.useState()
return (
<>
<img src={props.image} alt="" className="img-fluid img-thumbnail" />
<p>{props.title}</p>
<div onClick={() => props.handleFavouriteClick(value)}>
<i className="fa-regular fa-heart" value={value} onChange={setValue}></i>
</div>
</>
);
}
I'm doing pokedex (pokemon wiki stuff). I want to change my component view, when clicking on pokemon images (description lookalike). When I click on an image - nothing happens (firstly, I want at least pokemon's name to be added to the pokemonDescription array). What am I doing wrong?
let pokemonDescription = [];
const useForceUpdate = () => {
const [value, setValue] = useState(true);
return () => setValue(value => !value);
}
const forceUpdate = useForceUpdate();
const onPokemonClick = (event) => {
console.log(
"wrapper clicked, event.target - ",
event.target.getAttribute('data-name')
);
pokemonDescription = [];
pokemonDescription.push(event.target.getAttribute('data-name'));
console.log("description array -", pokemonDescription);
forceUpdate();
};
useEffect(() => {
document.querySelector(".wrapper").addEventListener("click", onPokemonClick);
...
return () => {
document.querySelector(".wrapper").removeEventListener("click", onPokemonClick);
};
}, []);
...
return (
<div className="Pokemons">
<div className="column pokemons-list">
<div className="wrapper">
{
pokemonsData.map((p, id) => (
<div className="box" key={ id }>
<img
src={ p.sprites.front_default }
alt="pokemon-img"
title={ p.name }
className="icon"
data-name={p.name}
/>
{ p.name}
<div className="container">
{ pokemonsTypes[id] }
</div>
</div>
))
}
</div>
...
</div>
<div className="column description">
{ pokemonDescription }
</div>
</div>
)
You should add pokemonDescription to your component state
const [pokemonDescription, setPokemonDescription] = useState([]);
Remove the forceUpdate function and hook, it is unnecessary.
Attach the click handlers to the elements with the data-name attribute you are trying to handle.
Map the pokemonDescription state array to renderable JSX. I simply used a div, but you should use whatever your UI design requires.
const onPokemonClick = (event) => {
setPokemonDescription(names => [
...names,
event.target.getAttribute('data-name'),
]);
};
...
return (
<div className="Pokemons">
<div className="column pokemons-list">
<div className="wrapper">
{
pokemonsData.map((p, id) => (
<div className="box" key={ id }>
<img
src={ p.sprites.front_default }
alt="pokemon-img"
title={ p.name }
className="icon"
data-name={p.name}
onClick={onPokemonClick} // <-- attach click handler to img element
/>
{ p.name}
<div className="container">
{ pokemonsTypes[id] }
</div>
</div>
))
}
</div>
...
</div>
<div className="column description">
{pokemonDescription.map(name => (
<div>{name}</div>
))}
</div>
</div>
)
Add pokemonDescription to state instead of some local variable and it will solve your issue.
Try to avoid using forceUpdate, most of the times it means only that you are doing something silly.
I don't what that useForceUpdate does , but here is how would go about adding pokemon names to description array which is a state variable in my answer
const [pokemonDescription , setPokemonDescription ] = useState(null);
const onPokemonClick = (p) => {
const tempPokemonDescription = [...pokemonDescription ];
pokemonDescription.push(p.name);
console.log("description array -", pokemonDescription);
setPokemonDescription(tempPokemonDescription )
};
...
return (
<div className="Pokemons">
<div className="column pokemons-list">
<div className="wrapper">
{
pokemonsData.map((p, id) => (
<div className="box" onClick={e=>onPokemonClick(p)} key={ id }>
<img
src={ p.sprites.front_default }
alt="pokemon-img"
title={ p.name }
className="icon"
/>
{ p.name}
<div className="container">
{ pokemonsTypes[id] }
</div>
</div>
))
}
</div>
...
</div>
<div className="column description">
{ pokemonDescription }
</div>
</div>
)
Currently i am practising my React skills. Now i am working on my Hooks knowledge. I have used a Coctail API to load some data. Just for fun i divided this up into three categories with a navigation bar.
When i click on the navigation items i go to the subcategories. But my classes won't update so the tab system doesn't work. You guys know what i have to do here? I tried this in a few different ways but none worked.
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const OverView = () => {
const [ term, setTerm ] = useState('Gin');
const [ categorie, setCategorie ] = useState('Cocktail');
const [ debouncedTerm, setDebouncedTerm ] = useState(term);
const [ results, setResults] = useState([]);
useEffect(() => {
const timerId = setTimeout(() => {
setDebouncedTerm(term);
}, 10);
return () =>{
clearTimeout(timerId);
};
}, [term]);
useEffect(() => {
const search = async () => {
const { data } = await axios({
'method':'GET',
'url':'https://the-cocktail-db.p.rapidapi.com/filter.php',
'headers':{
'content-type':'application/octet-stream',
'x-rapidapi-host':'the-cocktail-db.p.rapidapi.com',
'x-rapidapi-key':'49adf3a5ccmsh5d525b0005370d3p1b460bjsn7b0add17579e',
'useQueryString':true
},'params':{
'i': debouncedTerm
},
});
setResults(data.drinks);
};
search();
}, [debouncedTerm]);
const renderResults = results.map((result) => {
return (
<div key={result.idDrink} className="four wide column" style={{marginBottom:"20px"}}>
<a href={result.idDrink}>
<div className="card">
<img src={result.strDrinkThumb} width="250"/>
<div className="header">{result.strDrink}</div>
</div>
</a>
</div>
);
});
return (
<div className="ui grid">
<div className="row">
<h3>Select your favourate drink to get started</h3>
<div className="ui top attached tabular menu">
<div
className={'item active'}
onClick={(e) => setTerm('Gin') }>
Gin
</div>
<div
className={'item'}
onClick={(e) => setTerm('Vodka')}>
Vodka
</div>
<div
className={'item'}
onClick={(e) => setTerm('Whiskey')}>
Whiskey
</div>
</div>
</div>
<div className="row">
{renderResults}
</div>
</div>
);
};
export default OverView;
Thanks in advance.My first problem is that i don't know how to add an extra action upon my hook. I can't attach an extra onClick event and don't know how to add this to my useEfect functions.
Set the className based on the term state atom, simple as that.
I also refactored things a bit:
the result component is now, well, a separate component
searching is refactored into a separate function
import React, { useState, useEffect } from "react";
import axios from "axios";
const ResultCard = ({ result }) => (
<div className="four wide column" style={{ marginBottom: "20px" }}>
<a href={result.idDrink}>
<div className="card">
<img src={result.strDrinkThumb} width="250" />
<div className="header">{result.strDrink}</div>
</div>
</a>
</div>
);
async function doSearch(term) {
const { data } = await axios({
method: "GET",
url: "https://the-cocktail-db.p.rapidapi.com/filter.php",
headers: {
"content-type": "application/octet-stream",
"x-rapidapi-host": "the-cocktail-db.p.rapidapi.com",
"x-rapidapi-key": "49adf3a5ccmsh5d525b0005370d3p1b460bjsn7b0add17579e",
useQueryString: true,
},
params: {
i: term,
},
});
return data;
}
const OverView = () => {
const terms = ["Gin", "Vodka", "Whiskey"];
const [term, setTerm] = useState("Gin");
const [results, setResults] = useState([]);
useEffect(() => {
doSearch(term).then((data) => setResults(data.drinks));
}, [term]);
return (
<div className="ui grid">
<div className="row">
<h3>Select your favourate drink to get started</h3>
<div className="ui top attached tabular menu">
{terms.map((t) => (
<div
className={["item", term === t ? "active" : null].filter(Boolean).join(" ")}
onClick={(e) => setTerm(t)}
>
{t}
</div>
))}
</div>
</div>
<div className="row">
{results.map((result) => (
<ResultCard result={result} key={result.idDrink} />
))}
</div>
</div>
);
};
export default OverView;
You may want to look into the classnames module; the arcane [].filter().join() expression would become cx({item: true, active: t === term}) :)