I'm building an ecommerce site. Right now, I'm pulling items from an API and displaying them ("Shop" component). I can click on an item to go to an item page ("Item" component) with more details/information on the clicked item. When I click the "Add to shopping cart" button (in the "Item" component), the clicked item is displayed in the shopping cart screen ("Cart" component).
In order to move items from one page to another, I'm using React Router (see "App" component), and using <Link /> to display specific parameters. I use the parameters to pass the item ID (and quantity) so I can call the API for that specific item.
This works for one item, but how do I adjust my code to allow more than one item to be displayed in the shopping cart?
Greatly appreciate any feedback.
App component:
import React, { useState } from 'react';
import './App.css';
import Nav from './Nav';
import Shop from './Components/Shop';
import Info from './Components/Info';
import Cart from './Components/Cart';
import Item from './Components/Item';
import {BrowserRouter as Router, Switch, Route} from 'react-router-dom';
function App() {
return (
<Router>
<div className="App">
<Nav />
<Route path="/" exact component={Shop} />
<Route path="/Info" component={Info} />
<Route path="/Cart/:id/:qty" component={Cart} />
<Route path="/Item/:item" component={Item} />
</div>
</Router>
)
}
export default App;
Shop component:
import React, { useState, useEffect } from 'react';
import './../App.css';
import * as ReactBootStrap from 'react-bootstrap';
import {Link} from 'react-router-dom';
function Shop() {
const [products, setProducts] = useState([]);
const [filterProducts, setFilteredProducts] = useState([]);
const [item, setItem] = useState('');
const [currentSort, setCurrentSort] = useState('');
const [loading, setLoading] = useState(false);
useEffect(async () => {
fetchItems();
}, [])
const fetchItems = async () => {
const data = await fetch('https://fakestoreapi.com/products');
const items = await data.json();
setProducts(items)
setLoading(true)
}
function priceUSD(change){
return change.toFixed(2)
}
useEffect(() => {
const filteredItems = products.filter((a) => {
if (item === '') {return a} else {return a.category === item}
});
setFilteredProducts(filteredItems);
}, [item, products])
useEffect(() => {
if (currentSort === '') {
return
}
const sortedItems = filterProducts.sort((a, b) => {
return currentSort === 'ASE' ? a.price - b.price : b.price - a.price
});
setFilteredProducts([...sortedItems]);
}, [currentSort])
return (
<div>
<div className="itemSort">
<p onClick={() => setItem("")}>All items</p>
<p onClick={() => setItem("men clothing")}>Men clothing</p>
<p onClick={() => setItem("women clothing")}>Women clothing</p>
<p onClick={() => setItem("jewelery")}>Jewelery</p>
<p onClick={() => setItem("electronics")}>Electronics</p>
</div>
<div className="itemSort">
<p>Order by price</p>
<p onClick={() => setCurrentSort('DESC')}>Highest</p>
<p onClick={() => setCurrentSort('ASE')}>Lowest</p>
</div>
<div className="gridContainer">
{loading ?
(filterProducts.map((a, index) => (
<Link to={`/Item/${a.id}`}>
<div key={index} className="productStyle">
<img src={a.image} className="productImage"></img>
<p>{a.title}</p>
<p>${priceUSD(a.price)}</p>
</div>
</Link>
))) : (<ReactBootStrap.Spinner className="spinner" animation="border" />)
}
</div>
</div>
)
}
export default Shop;
Item component:
import React, { useState, useEffect } from 'react';
import {Link} from 'react-router-dom';
import './../App.css';
import * as ReactBootStrap from 'react-bootstrap';
function Item(props) {
const [product, setProduct] = useState([]);
const [loading, setLoading] = useState(false);
const [quantity, setQuantity] = useState(1);
const [cost, setCost] = useState([]);
useEffect(async () => {
fetchItems();
}, [])
const itemId = props.match.params.item;
const fetchItems = async () => {
const data = await fetch('https://fakestoreapi.com/products/' + itemId);
const items = await data.json();
setProduct(items)
setLoading(true)
setCost(items.price)
}
function priceUSD(change){
return change.toFixed(2)
}
useEffect(() => {
const newCost = quantity * product.price;
setCost(priceUSD(newCost))
}, [quantity])
return (
<div className="App">
<h2>Item</h2>
<div className="gridContainer">
{loading ?
(<div key={itemId} className="productStyle">
<img src={product.image} className="productImage"></img>
<p>{product.title}</p>
<p>{product.description}}</p>
<p>${priceUSD(product.price)}</p>
<div className="quantity">
<button className="btn minus-btn" type="button"
onClick={quantity > 1 ? () => setQuantity(quantity - 1) : null}>-</button>
<input type="text" id="quantity" placeholder={quantity}/>
<button className="btn plus-btn" type="button"
onClick={() => setQuantity(quantity + 1)}>+</button>
</div>
<Link to={`/Cart/${itemId}/${quantity}`}>
<button type="button">
Add to shopping cart ${cost}
</button>
</Link>
</div>
): (<ReactBootStrap.Spinner className="spinner" animation="border" />)
}
</div>
</div>
);
}
export default Item;
Cart component:
import React, { useState, useEffect } from 'react';
import './../App.css';
import * as ReactBootStrap from 'react-bootstrap';
function Cart(props) {
const [cart, setCart] = useState([]);
const [quantity, setQuantity] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(async () => {
fetchItems();
}, [])
const itemId = props.match.params.id;
const itemQuantity = props.match.params.qty;
const fetchItems = async () => {
const data = await fetch('https://fakestoreapi.com/products/' + itemId);
const items = await data.json();
setCart(items)
setQuantity(itemQuantity)
setLoading(true)
}
function price(qty){
const newPrice = qty * cart.price;
return newPrice
}
return (
<div>
{loading ? (
<div className="productStyle">
<img src={cart.image} className="productImage"></img>
<p>{cart.title}</p>
<div className="quantity">
<button className="btn minus-btn" type="button"
onClick={quantity > 1 ? () => setQuantity(quantity - 1) : null}>-</button>
<input type="text" id="quantity" placeholder={quantity}/>
<button className="btn plus-btn" type="button"
onClick={() => setQuantity(quantity + 1)}>+</button>
</div>
<p>${price(quantity)}</p>
</div>
) : (<ReactBootStrap.Spinner className="spinner" animation="border" />)}
</div>
);
}
export default Cart;
It is better if you persist the cart items in the localStorage.
In doing so, even when the user refreshes the tab, the app could load the data from the localStorage.
Example :-
Persisting data in the browser.
localStorage.setItem('my-app-cart-items', cartItems);
Retrieving data from the browser.
const cartItems = localStorage.setItem('my-app-cart-items');
Removing data from the browser
localStorage.removeItem('my-app-cart-items');
Related
Hi there I'm Beginner in React
I'm using React.js and trying to display Categories from public API and it showed up successfully in my app
Now I am trying to display the products for each category separately
So that if I click on a specific category, all the products for that category will appear on a separate page
App.js
import React from 'react';
import './App.css';
import {BrowserRouter,Routes,Route} from "react-router-dom";
import Categories from './components/Categories';
import Items from './components/Items';
function App() {
return (
<div className="App">
<BrowserRouter>
<Routes>
<Route path="/" exact element={<Categories />}/>
<Route path="/items" element={<Items />}/>
</Routes>
{/* <Items /> */}
</BrowserRouter>
</div>
);
}
export default App;
Categories.js
import React, { useState, useEffect } from 'react';
import '../style/Categories/categories.css'
import axios from 'axios'
const Categories = () => {
const [categories, setCategories] = useState([])
useEffect(() => {
const getCategory = async () => {
const res = await axios.get('https://api.publicapis.org/categories')
setCategories(res.data.categories)
console.log("res.data", res.data);
}
getCategory()
}, [])
return (
<>
<h1 style={{ textAlign: 'center' }}>All Categories</h1>
<div className="category__wrapper">
{categories.map(category =>
<div key={Math.random()} className="category__item">
<h2><a href="#" >{category}</a></h2>
</div>
)}
</div>
</>
)
}
export default Categories
Items.js
import React, { useState, useEffect } from 'react';
import axios from 'axios'
import '../style/Items/items.css'
const Items = () => {
const [items, setItems] = useState([])
useEffect(() => {
const getItems = async () => {
const result = await axios.get('https://api.publicapis.org/entries')
setItems(result.data.entries)
console.log("result.entries", result.data.entries);
}
getItems()
}, [])
return (
<div className="item__wrapper">
{items.map((item) => (
<div class="ui card" key={item.API}>
<div class="content">
<div class="header">{item.API}</div>
</div>
<div class="content">
<p>{item.Description}</p>
<span>Category: {item.Category}</span>
</div>
<div class="extra content">
<button class="ui button">Show More</button>
</div>
</div>
))}
</div>
)
}
export default Items
logancodemaker's answer is almost corret, but a few things missing.
Basically you need to send the Category to the Items component to be able to filter by category.
1-) Items route must be updated in App.js to accept url parameter.
<Route path="/items/:category" element={<Items />}/>
2-) We use Link component from the react-router-dom package in the Categories.js to dynamically send the category url parameter.
<Link to={`/items/${category}`} >{category}</Link>
3-) In Items.js we are filtering the products by the given category name.
import { useParams } from 'react-router-dom';
const {category} = useParams();
useEffect(() => {
const getItems = async () => {
const result = await axios.get('https://api.publicapis.org/entries');
const allItems = result.data.entries;
const categoryItems = allItems.filter(item => item.Category === category);
setItems(categoryItems)
}
getItems()
}, [category])
In App.js
- <Route path="/items" element={<Items />}/>
+ <Route path="/items/:categoryId" element={<Items />}/>
Categories.js
+ import { Link } from "react-router-dom"
- <h2><a href="#" >{category}</a></h2>
+ <h2>
+ <Link to={`/items/${category.id}`}>{category}</Link>
+ </h2>
Items.js
const Items = () => {
+ const { categoryId } = useParams()
const [items, setItems] = useState([])
useEffect(() => {
const getItems = async () => {
- const result = await axios.get('https://api.publicapis.org/entries')
+ const result = await axios.get(`https://api.publicapis.org/entries/${categoryId}`)
setItems(result.data.entries)
console.log("result.entries", result.data.entries);
}
getItems()
}, [])
I have React components :
Main.jsx
import { useState, useEffect } from "react";
import { Preloader } from "../Preloader";
import { Pokemons } from "../Pokemons";
import { LoadMore } from "../LoadMore";
function Main() {
const [pokemons, setPokemons] = useState([]);
const [loading, setLoading] = useState(true);
const [pokemonsPerPage] = useState("20");
const [pokemonOffset] = useState("0");
useEffect(function getPokemons() {
fetch(
`https://pokeapi.co/api/v2/pokemon?limit=${pokemonsPerPage}&offset=${pokemonOffset}`
)
.then((responce) => responce.json())
.then((data) => {
data.results && setPokemons(data.results);
setLoading(false);
});
}, []);
return (
<main className="container content">
{loading ? <Preloader /> : <Pokemons pokemons={pokemons} />}
<LoadMore />
</main>
);
}
export { Main };
LoadMore.jsx
import React from 'react'
function LoadMore() {
return (
<div className="button_container">
<a class="waves-effect waves-light btn-large" id="more">
More...
</a>
</div>
);
}
export { LoadMore };
I have created a button in the component. After clicking on it, the next 20 elements should be loaded. I created const [pokemonsPerPage] = useState("20"); and const [pokemonOffset] = useState("0"); in order to substitute these values into the request. The first is responsible for the number of objects on the page, the second is responsible for which element to start counting from. That is, now 20 elements are being output, starting from the very first one. (If you change const [pokemonOffset] = useState("0"); to 20, then the output will start from 21 elements). I'm sure that this is necessary for implementation, but I don't know what to do next
Help me complete this functionality
Here is what your Main.jsx should look like.
import { useState, useEffect } from "react";
import { Preloader } from "../Preloader";
import { Pokemons } from "../Pokemons";
import { LoadMore } from "../LoadMore";
function Main() {
const [pokemons, setPokemons] = useState([]);
const [loading, setLoading] = useState(true);
const [pokemonsPerPage] = useState(20);
const [page,setPage] = useState(1);
function getPokemons(pokemonOffset) {
fetch(
`https://pokeapi.co/api/v2/pokemon?limit=${pokemonsPerPage}&offset=${pokemonOffset}`
)
.then((responce) => responce.json())
.then((data) => {
data.results && setPokemons(data.results);
setLoading(false);
});
}
useEffect(() => {
const offset= page*pokemonsPerPage -pokemonsPerPage;
getPokemons(offset);
}, [page]);
return (
<main className="container content">
{loading ? <Preloader /> : <Pokemons pokemons={pokemons} />}
<LoadMore next={()=>setPage(p=>p+1)} />
</main>
);
}
export { Main };
while your Loadmore.jsx
import React from 'react'
function LoadMore(next) {
return (
<div className="button_container">
<a type="button" onclick={next} class="waves-effect waves-light btn-large" id="more">
More...
</a>
</div>
);
}
export { LoadMore };
I have a counter which needs to be updated before my DOM is rendered.
const [count, setCount] = useState(1);
I need this to be updated whenever user scrolls, and the function fetchMoreData is called.
So I added it inside Promise and waited until the same has been updated
const fetchMoreData = async () => {
await new Promise((resolve, reject) => {
setCount(count+1);
resolve();
});
updateNews();
};
But this does not seem to be working and count isn't incremented immediately.
How can I resolve the same?
EDIT : In the chat, A.mola helped me in solving the issue, so the answer he gave me was according to my code, I am posting the same here.
import React, { useEffect, useState } from "react";
import NewsItem from "./NewsItem";
import Spinner from "./Spinner";
import PropTypes from "prop-types";
import InfiniteScroll from "react-infinite-scroll-component";
const News = (props) => {
const [articles, setArticles] = useState([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [totalResults, setTotalResults] = useState(0);
const capitalizeFirstLetter = (string) => {
return string.charAt(0).toUpperCase() + string.slice(1);
};
const updateNews = async () => {
props.setProgress(10);
let goToPage = page;
const url = `newsapi.org/v2/…${props.country}&category=${props.category}&apiKey=${props.apiKey}&page=${goToPage}&pageSize=${props.pageSize}`;
props.setProgress(30);
let data = await fetch(url);
props.setProgress(50);
let parsedData = await data.json();
props.setProgress(70);
if (parsedData) {
setArticles(articles.concat(parsedData.articles));
setLoading(false);
setPage(page);
setTotalResults(parsedData.totalResults);
}
props.setProgress(100);
};
useEffect(() => {
updateNews();
// eslint-disable-next-line
}, []);
const fetchMoreData = async () => {
setPage((page) => {
console.log(page);
return page + 1;
});
console.log(page);
updateNews();
};
return (
<>
<h3 className="text-center" style={{ marginTop: "4%" }}>
NewsMonkey - Top {`${capitalizeFirstLetter(props.category)}`} Headlines
</h3>
{loading && <Spinner />}
<InfiniteScroll
dataLength={articles.length}
next={fetchMoreData}
hasMore={articles.length < totalResults}
loader={<Spinner />}
>
<div className="container">
<div className="row">
{articles.map((element) => {
return (
<div className="col-md-4" key={element.url}>
<NewsItem
title={
element && element.title ? element.title.slice(0, 45) : ""
}
description={
element && element.description
? element.description.slice(0, 50)
: ""
}
imageUrl={element.urlToImage}
newsUrl={element.url}
author={element.author}
date={element.publishedAt}
source={element.source.name}
/>
</div>
);
})}
</div>
</div>
</InfiniteScroll>
</>
);
};
Thanks a.mola for the comprehensive answer, I am updating the answer that you gave me in the chat here.
We are using useEffect hook to render our component using the array dependency.
import React, { useEffect, useState } from "react";
import NewsItem from "./NewsItem";
import Spinner from "./Spinner";
import PropTypes from "prop-types";
import InfiniteScroll from "react-infinite-scroll-component";
const News = ({ country, category, apiKey, pageSize, setProgress }) => {
const [articles, setArticles] = useState([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [totalResults, setTotalResults] = useState(0);
const capitalizeFirstLetter = (string) => {
return string.charAt(0).toUpperCase() + string.slice(1);
};
useEffect(() => {
const updateNews = async () => {
setProgress(10);
let goToPage = page;
const url = `newsapi.org/v2/…${country}&category=${category}&apiKey=${apiKey}&page=${goToPage}&pageSize=${pageSize}`;
setProgress(30);
let data = await fetch(url);
setProgress(50);
let parsedData = await data.json();
setProgress(70);
if (parsedData) {
setArticles(articles.concat(parsedData.articles));
setLoading(false);
setTotalResults(parsedData.totalResults);
}
setProgress(100);
};
updateNews();
}, [page, articles, pageSize, setProgress, apiKey, category, country]);
const fetchMoreData = () => setPage(page + 1);
return (
<>
<h3 className="text-center" style={{ marginTop: "4%" }}>
NewsMonkey - Top {`${capitalizeFirstLetter(category)}`} Headlines
</h3>
{loading && <Spinner />}
<InfiniteScroll dataLength={articles.length} next={fetchMoreData} hasMore={articles.length < totalResults} loader={<Spinner />}>
<div className="container">
<div className="row">
{articles.map((element) => {
return (
<div className="col-md-4" key={element.url}>
<NewsItem title={element && element.title ? element.title.slice(0, 45) : ""} description={element && element.description ? element.description.slice(0, 50) : ""} imageUrl={element.urlToImage} newsUrl={element.url} author={element.author} date={element.publishedAt} source={element.source.name} />
</div>
);
})}
</div>
</div>
</InfiniteScroll>
</>
);
};
Here is the codesandbox link : https://codesandbox.io/s/proud-shape-58yig?file=/Infinite.tsx:0-2597
I want to do an onClick counter but I have a problem with the counter iterating correctly. In the app there are 3 "products" and after clicking "Add To Cart" button the state of the object is updated but all of the products are generated separately. I think that is cousing the problem where the counter is different for each of the products or everything will work correctly if I lift the state up, but the console.log is just freshly generated for all of the products. I'm not really sure so I need help with that.
Here is some code in the order from the parent to the last child:
import { useEffect, useState } from "react";
import ProductList from "./ProductList";
const Products = () => {
const [products, setProducts] = useState (null);
useEffect (() => {
fetch('http://localhost:8000/products')
.then(res => {
return res.json();
})
.then(data => {
setProducts(data);
})
}, []);
return (
<div className="ProductList">
{products && <ProductList products={products}/>}
</div>
);
}
export default Products;
import Card from "./Card";
const ProductList = (props) => {
const products = props.products;
return (
<div className="ProductList" >
{products.map((product) => (
<Card product={product} key={product.id} />))}
</div>
);
}
export default ProductList;
import { useState } from "react";
const Card= ({ product }) => {
const [showDescription, setShowDescription] = useState(false);
const [CartCounter, setCartCounter ] = useState(0);
console.log(CartCounter);
return (
<div className="Product-Preview" >
<div className="backdrop" style={{ backgroundImage: `url(${product.image})` }}></div>
<h2>{product.title}</h2>
<div>{product.price}</div>
<button className="ShowDescription" onClick={() => setShowDescription(!showDescription)}>Details</button>
<button className="AddToCart" onClick={() => setCartCounter(CartCounter + 1)}>Add To Cart </button>
{showDescription && <p>{product.description}</p>}
<br />
</div>
);
};
export default Card;
Ok, you want to keep track of an aggregated value. I'll list code in some high level.
const ProductList = () => {
const [count, setCount] = useState(0)
const addOrRemove = n => { setCount(v => v + n) }
return products.map(p => <Card addOrRemove={addOrRemove} />)
}
const Card = ({ addOrRemove }) => {
// optional if you want to track card count
// const [count, setCount] = useState(0)
return (
<>
<button onClick={() => { addOrRemove(1) }>Add</button>
<button onClick={() => { addOrRemove(-1) }>Remove</button>
</>
)
}
Essentially either you track the local count or not, you need to let the parent to decide what is the final count, otherwise there'll be some out of sync issue between the child and parent.
I have this issue that I'm trying to fix, I have this piece of code that allows me to create a new blog:
import React, { useState, useEffect, useRef } from 'react'
import Blog from './components/Blog'
import Notification from './components/Notification'
import LoginForm from './components/LoginForm'
import BlogForm from './components/BlogForm'
import Togglable from './components/Togglabe'
import blogService from './services/blogs'
import loginService from './services/login'
import './App.css'
const App = () => {
const [blogs, setBlogs] = useState([])
const [user, setUser] = useState(null)
const [errorMessage, setErrorMessage] = useState(null)
const [showAll, setShowAll] = useState(true)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const blogFormRef = useRef()
useEffect(() => {
blogService
.getAll()
.then(initialBlogs => {
setBlogs(initialBlogs)
})
}, [])
// ----- Add blog function ------
const addBlog = async (blogObject) => {
blogFormRef.current.toggleVisibility()
const toAdd = blogService.createBlog(blogObject)
const returned = await toAdd
setBlogs(blogs.concat(returned))
}
// ----- Update Blog -----
const updateBlog = async (id) => {
const blog = blogs.find(blog => blog.id === id)
const toUpdate = { ...blog, likes: blog.likes + 1 }
const updateo = blogService.update(id, toUpdate)
const returned = await updateo
setBlogs(blogs.map(blog => blog.id !== id ? blog : returned))
}
// ----- Delete Blog -----
const delBlog = async (id) => {
const blogToDelete = blogService.deleteBlog(id)
const deleted = await blogToDelete
return deleted
}
// ----- Forms -------
const blogForm = () => (
<Togglable buttonLabel='new blog' ref={blogFormRef}>
<BlogForm
createBlog={addBlog}
/>
</Togglable>
)
return (
<div className="App">
<h2>Blogs</h2>
<Notification message={errorMessage}/>
{user === null ?
loginForm() :
<div>
<p>
{user.name} logged-in
<button onClick={(() => logOut())}>Log out</button>
</p>
{blogForm()}
</div>
}
<div>
<button onClick={() => setShowAll(!showAll)}>
{showAll ? 'hide' : 'show'}
</button>
</div>
<ul>
{showAll === true ?
blogs.map((blog, i) =>
<Blog
key={i}
blog={blog}
updateBlog={updateBlog}
delBlog={delBlog} />
)
: null
}
</ul>
</div>
)
}
export default App
and this other part is the form of the app that allows you to add a new blog
import React, { useState } from 'react'
const BlogForm = ({ createBlog }) => {
const [newTitle, setNewTitle] = useState('')
const [newAuthor, setNewAuthor] = useState('')
const [newContent, setNewContent] = useState('')
const handleTitle = (event) => {
setNewTitle(event.target.value)
}
const handleAuthor = (event) => {
setNewAuthor(event.target.value)
}
const handleContent = (event) => {
setNewContent(event.target.value)
}
const addBlog = (event) => {
event.preventDefault()
createBlog({
title: newTitle,
author: newAuthor,
content: newContent,
likes: 0
})
setNewTitle('')
setNewAuthor('')
setNewContent('')
}
return (
<div className="formDiv">
<form onSubmit={addBlog}>
<div>
<div>
Title:
<input
id="title"
type="text"
value={newTitle}
onChange={handleTitle}
/>
</div>
<div>
Author:
<input
type="text"
value={newAuthor}
onChange={handleAuthor}
/>
</div>
<div>
Content:
<input
type="text"
value={newContent}
onChange={handleContent}
/>
</div>
<button type="submit">Add</button>
</div>
</form>
</div>
)
}
export default BlogForm
So the problem is that I'm trying to add a new blog but it doesn't rerender the component and it doesn't show the new blog instantly. I have to refresh the page and then it appears. The funny thing is that it works with the updateBlog component which allows you to give a "LIKE" to some blog, but it doesn't react instantly with the AddBlog component.