This question already has an answer here:
React router 6 never unmount component when URL parameter changes
(1 answer)
Closed 13 days ago.
I am making an ecommerce website. If I click a product, the product full description will be displayed in a new page called detailPage and there will be similar product fetched from the API at the bottom of the detailPage. It is working fine till here. But if I click any of the similar product, I want the similar product to be displayed in the detailPage. How can I make it work?
import React, { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import useFetch from '../../Component/UseFetch/useFetch'
import { Link } from 'react-router-dom'
import './detailPage.css'
const DetailPage = () => {
const [item, setItem] = useState([])
const [mainImg, setMainImg] = useState('')
const [sameProduct, setSameProduct] = useState([])
const { loading, products } = useFetch('https://dummyjson.com/products')
const { id } = useParams()
useEffect(()=>{
const product = products.find(product => product.id === Number(id))
if(product){
setItem(product)
setMainImg(product.thumbnail)
}
},[products])
useEffect(()=>{
const allProduct = products.filter(product => product.category === item.category)
setSameProduct(allProduct)
}, [item, products])
return (
loading ? <div style={{
width: '100vw',
height: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '10vw'
}}>Loading...</div> :
<div>
<div>
<h1 className='product-headline'>Product Detail</h1>
</div>
<div className='product-box'>
<div>
{
products && item && (
<div style={{height: '320px'}}>
<img className='main-image' src={mainImg} />
</div>
)
}
<div className='short-img-box'>
{
products && item && item.images && item.images.map((image, id)=>{
return <img className='short-img' key={id} src={image} onClick={()=> setMainImg(image) }/>
})
}
</div>
</div>
<div>
{
products && item && item.price && !isNaN(item.price) && (
<div>
<h1>{item.title}</h1>
<div>
<span>Rating: {item.rating}</span>
<span>Available Stock: {item.stock}</span>
</div>
<div>
<span>{parseInt(item.price + item.price / 100 * 25)}</span><br></br>
<span>{item.price}</span>
</div>
<p>{item.description}</p>
</div>
)
}
<button>Add to cart</button>
</div>
</div>
<div>
<h1 style={{textAlign: 'center'}}>Review section</h1>
</div>
<div>
<h1 className='product-headline'>Similar Products</h1>
<div className='products'>
{
sameProduct.map((product)=>{
const {id, title, description, category, price, thumbnail, images, rating, stock} = product
return (
<div key={id} className='product'>
<Link to={`/detailpage/${id}`}><img src={thumbnail} /></Link>
<div className='product-details'>
<Link className='title' to={`/detailpage/${id}`}><h3>{title}</h3></Link>
<div className='price-rating'>
<p>${price}</p>
<p>Rating: {rating}/5</p>
</div>
</div>
<button className='add-cart'>ADD TO CART</button>
<span className='product-stock'>Stock: {stock}</span>
</div>
)
})
}
</div>
</div>
</div>
)
}
export default DetailPage
It looks to me that your only problem is that id is not included in your useEffect's dependency array. Can you confirm that that the url is updated to the new product id when you click the similar product?
In that case, all you should do is include the id in the dependency array like this to make the useEffect re-run when the id in the URL is changed:
useEffect(()=>{
const product = products.find(product => product.id === Number(id))
if(product){
setItem(product)
setMainImg(product.thumbnail)
}
},[products, id])
I think a good solution for this should be something like this (not find product in array of product, but fetch single product in single request (for details page):
You will add useEffect hook to handle, when id of product changed in URL.
useEffect(()=> {
// i change name of the function, beacuse it's impossible to use hooks inside callbacks
const {loading, poduct} = someFetchFunction('https://dummyjson.com/products');
// save your data in `useReducer` hook (but it's up to you)
}, [id]);
So right now when you change url on this page, this useEffect hook will trigger and get current product
You also have a way to change the useFetch hook, and move useEffect to this hook, so when something changes it will trigger a request, but from my perspective of view my first variant is better
Related
let me explain my situation.
I am building a MERN project to my portfolio and I am trying to make a button toggle between the name of an item and a inputfield. So when the user click the pen (edit), it will add a class with the displain:none; in the div with the text coming from the MongoDB data base to hide it and will remove it from the div with the input. I could manage to do it. BUT since the amount of items can inscrease, clicking in one of them cause the toggle in all of them.
It was ok until I send some useState as props to the component.
This is my code from the App.jsx
import React, {useState, useEffect} from "react";
import Axios from "axios";
import "./App.css";
import ListItem from "./components/ListItem";
function App() {
//here are the use states
const [foodName, setFoodName] = useState("");
const [days, setDays] = useState(0);
const [newFoodName, setNewFoodName] = useState("");
const [foodList, setFoodList] = useState([]);
//here is just the compunication with the DB of a form that I have above those components
useEffect(() => {
Axios.get("http://localhost:3001/read").then((response) => {
setFoodList(response.data);
});
}, []);
const addToList = () => {
Axios.post("http://localhost:3001/insert", {
foodName: foodName,
days: days,
});
};
const updateFood = (id) => {
Axios.put("http://localhost:3001/update", {
id: id,
newFoodName: newFoodName,
});
};
return (
<div className="App">
//Here it starts the app with the form and everything
<h1>CRUD app with MERN</h1>
<div className="container">
<h3 className="container__title">Favorite Food Database</h3>
<label>Food name:</label>
<input
type="text"
onChange={(event) => {
setFoodName(event.target.value);
}}
/>
<label>Days since you ate it:</label>
<input
type="number"
onChange={(event) => {
setDays(event.target.value);
}}
/>
<button onClick={addToList}>Add to list</button>
</div>
//Here the form finishes and now it starts the components I showed in the images.
<div className="listContainer">
<hr />
<h3 className="listContainer__title">Food List</h3>
{foodList.map((val, key) => {
return (
//This is the component and its props
<ListItem
val={val}
key={key}
functionUpdateFood={updateFood(val._id)}
newFoodName={newFoodName}
setNewFoodName={setNewFoodName}
/>
);
})}
</div>
</div>
);
}
export default App;
Now the component code:
import React from "react";
//Material UI Icon imports
import CancelIcon from "#mui/icons-material/Cancel";
import EditIcon from "#mui/icons-material/Edit";
//import CheckIcon from "#mui/icons-material/Check";
import CheckCircleIcon from "#mui/icons-material/CheckCircle";
//App starts here, I destructured the props
function ListItem({val, key, functionUpdateFood, newFoodName, setNewFoodName}) {
//const [foodList, setFoodList] = useState([]);
//Here I have the handleToggle function that will be used ahead.
const handleToggle = () => {
setNewFoodName(!newFoodName);
};
return (
<div
className="foodList__item"
key={key}>
<div className="foodList__item-group">
<h3
//As you can see, I toggle the classes with this conditional statement
//I use the same classes for all items I want to toggle with one click
//Here it will toggle the Food Name
className={
newFoodName
? "foodList__item-newName-delete"
: "foodList__name"
}>
{val.foodName}
</h3>
<div
className={
newFoodName
? "foodList__item-newName-group"
: "foodList__item-newName-delete"
}>
//Here is the input that will replace the FoodName
<input
type="text"
placeholder="The new food name..."
className="foodList__item-newName"
onChange={(event) => {
setNewFoodName(event.target.value);
}}
/>
//Here it will confirm the update and toggle back
//Didn't implement this yet
<div className="foodList__icons-confirm-group">
<CheckCircleIcon
className="foodList__icons-confirm"
onClick={functionUpdateFood}
/>
<small>Update?</small>
</div>
</div>
</div>
//here it will also desappear on the same toggle
<p
className={
newFoodName
? "foodList__item-newName-delete"
: "foodList__day"
}>
{val.daysSinceIAte} day(s) ago
</p>
<div
className={
newFoodName
? "foodList__item-newName-delete"
: "foodList__icons"
}>
//Here it will update, and it's the button that toggles
<EditIcon
className="foodList__icons-edit"
onClick={handleToggle}
/>
<CancelIcon className="foodList__icons-delete" />
</div>
</div>
);
}
export default ListItem;
I saw a solution that used different id's for each component. But this is dynamic, so if I have 1000 items on the data base, it would display all of them, so I can't add all this id's.
I am sorry for the very long explanation. It seems simple, but since I am starting, I spent the day on it + searched and tested several ways.
:|
I have been trying to find a way so that when I click a button the next movie in the array shows up on the screen. I am new to react so please forgive my code. I think my problem is in how I am fetching data from the external site. I am not sure how/when I should load in the data so that it functions most effeciently. Any help or tips would be greatly appreciated
function App() {
const [items,setItems] = useState([]);
async function getItems() {
const response = await fetch('https://etbd.tech/nuspljr_334360/csv2json.php');
const data = await response.json();
setItems([...data]);
}
useEffect(() => {
getItems();
},[]);
async function loadData() {
const response = await fetch('https://etbd.tech/nuspljr_334360/sread.php?f=imdb_top_1000.csv');
const menu = await response.json();
setItems([...menu]);
}
useEffect(() => {
loadData();
},[]);
function Buttons() {
return (
<div>
<button className="button" onClick={nextMovie}>Next Movie</button>
</div>
)
}
function RenderItem() {
return (
<div>
{items.map((item) => (
<div key={v4()} className='card'>
<section className="description">
<img src={item.Poster_Link} alt="Poster_Image"/>
<section className="title">
<p>{item.Series_Title} ({item.Released_Year})</p>
<p>IMDB Rating: {item.IMDB_Rating}</p>
</section>
<p>{item.Overview}</p>
<p>{item.Genre} - {item.Runtime}</p>
</section>
</div>
))}
</div>
)};
return (
<div className="body">
<h2>Guess that Movie</h2>
<div className="table">
<RenderItem />
<div className="nav">
<Buttons />
</div>
</div>
</div>
);
}
export default App;
Okay, some things missing in your code.
First of all, you are referencing to nextMovie function and did not declare it.
After this, you should think about the logic of your component and how it should behave.
You do have a state for storing your items that you are fetching.
Now you need another state to store some kind of logic to display the current movie and when you click the next movie button, to update this state with the next one.
You can set a const [activeIndex, setActiveIndex] = useState(-1);
When you first fetch your items, you can set movieIndex to 0.
And inside your component render item, you will get rid of the items.map; because you don't want to loop inside your array, you just want to show one.
We are now passing the active movie as a prop to RenderItem component and showing the active movie data.
I recommend that you learn more about javascript before trying react code.
You can see a working demo here: https://codesandbox.io/s/optimistic-sid-8morgf?file=/src/App.js
import React, {useState, useEffect} from 'react'
function App() {
const [items,setItems] = useState([]);
const [activeIndex,setActiveIndex] = useState(-1);
async function getItems() {
const response = await fetch('https://etbd.tech/nuspljr_334360/csv2json.php');
const data = await response.json();
setActiveIndex(0);
setItems([...data]);
}
useEffect(() => {
getItems();
},[]);
function Buttons() {
return (
<div>
<button className="button" onClick={nextMovie}>Next Movie - next index {activeIndex + 1}</button>
</div>
)
}
function RenderItem({item}) {
return (
<div>
<div className='card'>
<section className="description">
<img src={item.Poster_Link} alt="Poster_Image" />
<section className="title">
<p>{item.Series_Title} ({item.Released_Year})</p>
<p>IMDB Rating: {item.IMDB_Rating}</p>
</section>
<p>{item.Overview}</p>
<p>{item.Genre} - {item.Runtime}</p>
</section>
</div>
</div>
)};
const nextMovie = () => setActiveIndex((prev) => prev + 1);
return (
<div className="body">
<h2>Guess that Movie</h2>
<div className="table">
{items.length === 0 ? 'Loading...' : (<>
<RenderItem item={items[activeIndex]} />
<div className="nav">
<Buttons />
</div>
</>)}
</div>
</div>
);
}
export default App;
I am building my website portfolio using React and Sanity. This actually is my first project with React. The idea was to use sanity in order to store data that I can use on my website, such as "projects" and so far everything is going well, except for one thing: THE BUTTON IS TARGETING WRONG DATA.
The projects are divided in categories: UX/UI - React - JavaScript - University Projects - All
Everything is working fine, the tags imported from sanity's schemas allow me to categorise the projects.
Every project looks like a little card and when hovered, there is a little description as long as the button "MORE+".
HERE IS THE PROBLEM
When I click the button, there is a big window showing up where I can see what is the project about.
Right now there are two projects on sanity (let's call them A and B).
Project A is categorised as JavaScript and project B as React and UI/UX.
If I hover on project A and Click the button "MORE+", it would open project B on the big window, why is that?
This happens only when I am in the category "ALL" but I assume it doesn't happen in other categories only because there is only one project each category, while in "ALL" both projects are shown.
I leave below the code that I used for the button and how I imported this from sanity.
It may look a bit confusing and long, only because I used a lot of motion frame and wrapped everything in a lot of div
Also in few point it is still uncomplete.
import React, { useState, useEffect } from 'react';
import {AiFillEye, AiFillGithub} from 'react-icons/ai';
import {motion} from 'framer-motion';
import './Work.scss';
import { HiX } from 'react-icons/hi';
import { AppWrap } from '../../wrapper';
import {urlFor, client} from '../../client';
const Work = () => {
const [works, setWorks] = useState([]);
const [filterWork, setFilterWork] = useState([]);
const [activeFilter, setActiveFilter] = useState('All');
const [animateCard, setAnimateCard] = useState({ y: 0, opacity: 1 });
const [toggle, setToggle] = useState(false);
useEffect(() => {
const query = '*[_type == "works"]';
client.fetch(query).then((data) => {
setWorks(data);
setFilterWork(data);
});
}, []);
const handleWorkFilter = (item) => {
setActiveFilter(item);
setAnimateCard([{ y: 100, opacity: 0 }]);
setTimeout(() => {
setAnimateCard([{ y: 0, opacity: 1 }]);
if (item === 'All') {
setFilterWork(works);
} else {
setFilterWork(works.filter((work) => work.tags.includes(item)));
}
}, 500);
};
return (
<>
<h2 className="portfolio-head-text">My <span>Portfolio</span></h2>
<div className="app__work-filter">
{['UI/UX','JavaScript', 'React JS', 'University Projects', 'All'].map((item, index) => (
<div key={index}
onClick={() => handleWorkFilter(item)}
className={`app__work-filter-item app_flex p-text ${activeFilter === item ? 'item-active' : ''}`}>
{item}
</div>
))}
</div>
<motion.div
animate={animateCard}
transition={{duration:0.5, delayChildren: 0.5}}
className="app__work-portfolio"
>
{filterWork.map((work,index) => (
<div className="app__work-card-container" key={index}>
<div className="app__work-item app__flex">
<div className="app__work-img app__flex">
<img src={urlFor(work.imgUrl1)} alt={work.name}/>
<motion.div
whileHover={{opacity:[0,1]}}
transition={{duration: 0.3, ease: 'easeInOut', staggerChildren: 0.6}}
className="app__work-hover app__flex">
<p>{work.descriptionPreview}</p>
<motion.div
whileInView={{scale:1}}
whileHover={{scale:[1,0.9]}}
transition={{duration: 0.2}}
className="app__flex"
>
<button onClick={() => setToggle(true)}>more+</button>
</motion.div>
</motion.div>
</div>
<div className="app__work-content app__flex">
<h4 className="bold-text">{work.title}</h4>
<p className="p-text" style={{marginTop: 10}}>{work.tagView}</p>
</div>
</div>
{toggle &&(
<div className="app__work-big-window">
<div className="window-img-x">
<img classname="window-img" src={urlFor(work.imgUrl1)} alt={work.name}/>
<div><HiX className="window-x" onClick={() => setToggle(false)}/></div>
</div>
<div>
<h4>{work.title}</h4>
<h6>{work.subTitle}</h6>
<div/>
<p>{work.description}</p>
</div>
<div/>
<div>
<h6>Technologies used: </h6>
<p>{work.tech}</p>
</div>
</div>
)}
</div>
))}
</motion.div>
</>
)
}
i am trying to save favorite post ids in an array with button click. The thing is currently it is only saving one ID at a time in array and when you click you on another "click here" button , it removes the previous id and show you the current id. Currently my array is not saving previous saved Ids with the new one. I will appreciate it if someone explain why it is not working and is there something wrong in my code while saving the ids.
Screenshot
Code
import React, { useEffect } from 'react'
import Demopic from "../assets/img/demopic/4.jpg";
import { useState, useRef } from 'react';
import { post } from 'jquery';
export default function Post(props) {
const { id, title, body } = props.data;
const [postid,setPostID] = useState([]);
const [isActive, setActive] = useState(false);
function onHandleOnClick(id) {
setPostID([...postid, id]);
};
useEffect(()=>{
console.log(postid);
},[postid]);
return (
<div className="card">
<div className="row">
<div className="col-md-5 wrapthumbnail">
<a href="post.html">
<div className="thumbnail dd" style={{ backgroundImage: 'url(' + Demopic + ')' }}>
</div>
</a>
</div>
<div className="col-md-7">
<div className="card-block">
<h2 className="card-title"><a href={'/post-detail/'+id} key={id}>{title}</a></h2>
<h4 className="card-text">
{body.length > 100 ? `${body.substring(0, 90)}...` : body }
</h4>
<div className="metafooter">
<div className="wrapfooter text-left">
<span className="meta-footer-thumb">
<a href="author.html">
<img className="author-thumb" src="https://www.gravatar.com/avatar/e56154546cf4be74e393c62d1ae9f9d4?s=250&d=mm&r=x" alt="Sal"/></a>
</span>
<span className="author-meta">
<span className="post-name">Steve</span><br/>
<span className="post-date">22 July 2017</span><span className="dot"></span><span className="post-read">6 min read</span>
</span>
<button onClick={(e)=>onHandleOnClick(id)} value={id}>click here</button>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
looks like your Post component is a single post, therefore each one of the Post components that you are creating will have their own postId array.
so if you have 10 posts you will have 10 different arrays, to test this, you can click on the same button multiple times and you will see that they array of that component will grow, for example the Id is 4 and you click multiple times you will see [4 ,4, 4, 4.....] and then if you do it on the Id 2 you will see [2, 2, 2, 2....].
to fix this you should have on your parent component the array and then in then pass the update function to your childs, something like this:
const ParentComponent = () => {
const [postid,setPostID] = useState([]);
const [isActive, setActive] = useState(false);
function onHandleOnClick(id) {
setPostID([...postid, id]);
};
useEffect(()=>{
console.log(postid);
},[postid]);
return (<>
<Post clickFavourite={onHandleOnClick} ...other props/>
<Post clickFavourite={onHandleOnClick} ...other props/>
<Post clickFavourite={onHandleOnClick} ...other props/>
<Post clickFavourite={onHandleOnClick} ...other props/>
<Post clickFavourite={onHandleOnClick} ...other props/>
</>)
}
and then in your child component (Post) you should remove the state and the handler and just use the parent function:
function Post(props) {
const { id, title, body } = props.data;
const { clickFavourite } = props;
return (
<button onClick={(e)=>clickFavourite(id)} value={id}>click here</button>
)
}
notice that I deleted a lot of the content of the Post component just to read it easily.
also take into account that this can be done in different ways, this was the first way that came into my mind but you can use global states, states managers, hooks, etc etc. but as far as I see this is the easiest way and will get the job done
Use
const [postid,setPostID] = useState({});
Instead of
const [postid,setPostID] = useState([]);
It will work.
I have been struggling with this for some time and I am not sure how to solve the issue.
Basically, I am trying to render some components onto my Index page, this is my code below:
App.js
import Index from "./Components/Index"
import axios from "axios"
export default function App() {
const [movieList, setMovieList] = React.useState([])
let featured = []
let coming = []
let showing = []
React.useEffect(() => {
console.log("Ran App Effects")
axios.get(`API_CALL_TO_GET_LIST_OF_MOVIES`)
.then(res =>{
setMovieList(res.data)
})
}, [])
return(
<div>
{movieList.map(movie =>{
if(movie.status === 'featured'){
featured.push(movie.api_ID)
} else if (movie.status === 'upcoming'){
coming.push(movie.api_ID)
} else{
showing.push(movie.api_ID)
}
})}
<Index featured={featured} coming={coming} showing={showing}/>
</div>
)
}
In the code above I am receiving an array of Objects and based on what is in their status I am putting them in some empty arrays and sending them as props into my Index component.
This is what my index component looks like:
import React from "react"
import Header from "./Header"
import Footer from "./Footer"
import MovieCard from "./MovieCard"
import axios from "axios"
export default function Index(props) {
const [featuredMovies, setFeaturedMovies] = React.useState([])
const [comingMovies, setComingMovies] = React.useState([])
//const featured = [419704,338762,495764,38700,454626,475557]
//const coming = [400160,514847,556678,508439,524047,572751]
React.useEffect(() => {
console.log("Ran Effect")
axios.all(props.featured.map(l => axios.get(`API_CALL_TO_GET_SPECIFIC_MOVIE/${l}`)))
.then(axios.spread(function (...res){
setFeaturedMovies(res)
}))
.catch((err) => console.log(err))
axios.all(props.coming.map(l => axios.get(`API_CALL_TO_GET_SPECIFIC_MOVIE/${l}`)))
.then(axios.spread(function (...res){
setComingMovies(res)
}))
.catch((err) => console.log(err))
}, [])
return(
<body>
<Header />
<section className="home">
<div className="container">
<div className="row">
<div className="col-12">
<a className="home__title">FEATURED MOVIES</a>
</div>
{ featuredMovies.map(movie =>{
return <MovieCard movie={movie} featured={true} />
}) }
{console.log(props.featured)}
</div>
</div>
</section>
<section className="home">
<div className="container">
<div className="row">
<div className="col-12">
<a className="home__title">COMING SOON</a>
</div>
{ comingMovies.map(movie =>{
return <MovieCard movie={movie} featured={false} />
})}
</div>
</div>
</section>
<Footer/>
</body>
)
}
The issue I am running into is, whenever I run the app for the first time it works fine but then when I hit the refresh button the components do not render anymore
The only time it re-renders when I refresh the page is when I uncomment,
//const featured = [419704,338762,495764,38700,454626,475557]
//const coming = [400160,514847,556678,508439,524047,572751]
and replace the props.featured.map and props.coming.map with featured.map and coming.map hence using the hard coded values and not the values passed in from the props.
Any help with this would be much appreciated as I am completely stuck and currently pulling my hair out.
I took the liberty to tinker with your code. In the example below I've rearranged the data into three sets with the help of useMemo and by checking the status property of each movie. It is important to keep any data related logic outside of the render logic.
I also moved around some of your HTML structure. You were outputting a <body> tag inside of a <div>. The outer layer should be in control of the outer HTML structure, so I moved that HTML to the App component.
import { useState, useEffect, useMemo } from 'react'
import Header from "./Components/Header"
import Footer from "./Components/Footer"
import Index from "./Components/Index"
import axios from "axios"
export default function App() {
const [movieList, setMovieList] = useState([])
const featuredMovies = useMemo(() => {
return movieList.filter(({ status }) => status === 'featured');
}, [movieList]);
const upcomingMovies = useMemo(() => {
return movieList.filter(({ status }) => status === 'upcoming');
}, [movieList]);
const showingMovies = useMemo(() => {
return movieList.filter(({ status }) => status !== 'featured' && status !== 'upcoming');
}, [movieList]);
useEffect(() => {
axios.get(`API_CALL_TO_GET_LIST_OF_MOVIES`)
.then(res =>{
setMovieList(res.data)
})
}, [])
return (
<body>
<Header />
<Index data={featuredMovies} title="Featured Movies" featured={true} />
<Index data={upcomingMovies} title="Coming Soon" />
<Index data={showingMovies} title="Showing Now" />
<Footer/>
</body>
)
}
Since we now have three sets of movies (featured, upcoming, and playing) it would also make sense to have three components that handle those data sets instead of having one that handles multiple. Each Index component gets its own data set and other props to render the movies within it.
import MovieCard from "./MovieCard"
export default function Index({ data, title, featured = false }) {
return (
<section className="home">
<div className="container">
<div className="row">
<div className="col-12">
<a className="home__title">{title}</a>
</div>
{data.map(movie => {
return <MovieCard movie={movie} featured={featured} />
})}
</div>
</div>
</section>
);
}