How can I toggle one icon which triggers the disabling of another icon in React? - javascript

I am developing an app with react and redux toolkit. I am using an API. the part that I would like help with is this:
I have a favorite Icon. this icon takes a copy of the movie and displays it in the favorite section. There is another icon which is for watched movies. so the idea is that if I click on watched it should disable only the favorite icon of the movie card I clicked. however, it disables all favorite icons for all movie cards. The watched icon is referred to as Eye the favorite icon is referred to as star
This is the eye component (Watched)
import { useState } from "react";
import { BsEyeSlash, BsEyeFill } from "react-icons/bs";
import { useDispatch, useSelector } from "react-redux";
import {
add_watched,
remove_watched,
add_occupied,
remove_occupied,
} from "../features/animeSlice";
const Eye = ({ anime, type }) => {
const [active_eye, setActive_eye] = useState(false);
const dispatch = useDispatch();
const toggle_eye = () => {
if (!active_eye) {
setActive_eye((prev) => {
return !prev;
});
dispatch(add_watched(anime));
dispatch(add_occupied(type));
} else {
setActive_eye((prev) => {
return !prev;
});
dispatch(remove_watched(anime?.mal_id));
dispatch(remove_occupied());
}
};
return (
<div>
{!active_eye ? (
<BsEyeSlash
className="text-xl text-green-500 icons_1"
onClick={toggle_eye}
/>
) : (
<BsEyeFill
className={"text-xl text-red-500 icons_1"}
onClick={toggle_eye}
/>
)}
</div>
);
};
export default Eye;
This is the Star Component (Favorite)
import { useState } from "react";
import { AiOutlineStar, AiFillStar } from "react-icons/ai";
import { add_favourite, remove_favourite } from "../features/animeSlice";
import { useDispatch, useSelector } from "react-redux";
const Star = ({ anime }) => {
const { value} = useSelector((state) => state.anime);
const [active_star, setActive_star] = useState(false);
const dispatch = useDispatch();
const toggle_star = () => {
if (!active_star) {
setActive_star((prev) => {
return !prev;
});
dispatch(add_favourite(anime));
} else {
setActive_star((prev) => {
return !prev;
});
dispatch(remove_favourite(anime?.mal_id));
}
};
return (
<div>
{!active_star ? (
<AiOutlineStar
className={
value === "occupied"
? "text-xl text-gray-300 pointer-events-none"
: "text-xl text-yellow-500 icon_1"
}
onClick={toggle_star}
/>
) : (
<AiFillStar
className={
value === "occupied"
? "text-xl text-gray-300 pointer-events-none"
: "text-xl text-yellow-500 icon_1"
}
onClick={toggle_star}
/>
)}
</div>
);
};
export default Star;
this is the redux toolkit slice
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
favourite: [],
watched: [],
value: "",
page:""
};
const animeSlice = createSlice({
name: "anime",
initialState,
reducers: {
add_favourite(state, { payload }) {
state.favourite.push(payload);
},
remove_favourite(state, { payload }) {
state.favourite = state.favourite.filter(
(anime) => anime?.mal_id !== payload
);
},
add_watched(state, { payload }) {
state.watched.push(payload);
},
remove_watched(state, { payload }) {
state.watched = state.watched.filter((anime) => anime?.mal_id !== payload);
},
add_occupied(state, { payload }) {
state.value = payload;
},
remove_occupied(state) {
state.value = "";
},
pageNumber(state, { payload }) {
state.page = payload
}
},
});
export const {
add_favourite,
remove_favourite,
add_watched,
remove_watched,
add_occupied,
remove_occupied,
pageNumber
} = animeSlice.actions;
export default animeSlice.reducer;
This is the component that holds the component of eye and star
import { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import Eye from "./Eye";
import Star from "./Star";
const TopAnime = ({ anime }) => {
//pagination
//
//
let colorYear = (year) => {
if (year === "N/A") {
return "text-violet-500";
} else {
return "";
}
};
return (
<section className="grand px-4 pt-2 pb-5 w-64 bg-white/5 bg-opacity-80 backdrop-blur-sm rounded-lg cursor-pointer font-poppins animate-slideup">
<div className="pb-1 wrapper_icons">
<div className="wrapper_hover">
<Eye anime={anime} type="occupied" />
<Star anime={anime} />
</div>
</div>
<Link to={`/anime/${anime?.mal_id}`}>
<div className="wrapper_1 flex flex-col items-center justify-center">
<div className="h-[313px] w-[219px]">
<img
src={anime?.images?.jpg?.large_image_url}
alt={anime?.title}
className="h-full w-full"
/>
</div>
</div>
<div className="flex flex-col mt-3">
<p className="text-sm text-white truncate mx-1">
{anime?.title_english ? anime?.title_english : anime?.title}
</p>
<div className="flex justify-between items-center text-sm text-yellow-500 mx-1">
<p className={colorYear(anime?.year ? anime?.year : "N/A")}>
{anime?.year ? anime?.year : "N/A"}
</p>
<p
className={
anime?.score <= 7
? "text-cyan-500"
: anime?.score <= 5
? "text-red-600"
: "text-green-500"
}
>
{anime?.score}
</p>
</div>
</div>
</Link>
</section>
);
};
export default TopAnime;
This is where TopAnime is rendered
import { useGetAnimeQuery } from "../features/API";
import { useDispatch, useSelector } from "react-redux";
import { useState, useEffect } from "react";
import TopAnime from "../components/TopAnime";
import Spinner from "../components/Spinner";
import { NavLink, Link } from "react-router-dom";
import Paginator from "../components/Paginator";
//import ReactPaginate from "react-paginate";
import Pagination from "#mui/material/Pagination";
const Home = () => {
const [page, setPage] = useState(1);
const {
data: manga = [],
isLoading,
isFetching,
error,
} = useGetAnimeQuery(page);
const { data, pagination } = manga;
//destructuring objects
//const { data, pagination } = manga;
//const top_anime = data;
const total = Math.ceil(pagination?.items?.total / 24)
//const current_page = pagination?.current_page;
//const per_page = pagination?.items?.per_page;
//const { items: pages } = total;
/* let fetchData = async (page = 1) => {
let res = await fetch(
`https://api.jikan.moe/v4/top/anime?page=${page}&limit=24`
);
let query = await res.json();
const { data, pagination } = query;
let totalPages = Math.ceil(pagination?.items.total / 24);
setPageCount(totalPages);
setData(data);
};
useEffect(() => {
fetchData();
}, []);
*/
import { useGetAnimeQuery } from "../features/API";
import { useDispatch, useSelector } from "react-redux";
import { useState, useEffect } from "react";
import TopAnime from "../components/TopAnime";
import Spinner from "../components/Spinner";
import { NavLink, Link } from "react-router-dom";
import Paginator from "../components/Paginator";
//import ReactPaginate from "react-paginate";
import Pagination from "#mui/material/Pagination";
const Home = () => {
const [page, setPage] = useState(1);
const {
data: manga = [],
isLoading,
isFetching,
error,
} = useGetAnimeQuery(page);
const { data, pagination } = manga;
//destructuring objects
//const { data, pagination } = manga;
//const top_anime = data;
const total = Math.ceil(pagination?.items?.total / 24)
//const current_page = pagination?.current_page;
//const per_page = pagination?.items?.per_page;
//const { items: pages } = total;
/* let fetchData = async (page = 1) => {
let res = await fetch(
`https://api.jikan.moe/v4/top/anime?page=${page}&limit=24`
);
let query = await res.json();
const { data, pagination } = query;
let totalPages = Math.ceil(pagination?.items.total / 24);
setPageCount(totalPages);
setData(data);
};
useEffect(() => {
fetchData();
}, []);
*/
const handleChange = (event, value) => {
setPage(value);
};
const display = data?.map((anime) => (
<TopAnime anime={anime} key={anime.mal_id} />
));
//const pageCount = Math.ceil(pagination?.items?.total / 24);
if (isLoading) {
return <Spinner color_1={"#141e30"} color_2={"#243b55"} />;
} else if (isFetching) {
return <Spinner color_1={"#141e30"} color_2={"#243b55"} />;
} else if (error) {
console.log("ERROR =>", error.message);
}
return (
<section className="bg-gradient-to-r from-[#141e30] to-[#243b55]">
<div className="container font-poppins">
<div className="grid grid-cols-4 gap-3 place-items-center px-20">
{/* {top_anime &&
top_anime?.map((anime) => (
<TopAnime anime={anime} key={anime.mal_id} />
))} */}
{display}
</div>
<div className="button text-yellow-500 flex items-center justify-center mt-2 pb-2 cursor-pointer">
{/* <Paginator paginated={paginated} NumP={pagination?.last_visible_page} /> */}
{/* <ReactPaginate
previousLabel={"Previous"}
nextLabel={"Next"}
onPageChange={(page) => fetchData(page.selected + 1)}
pageCount={pageCount}
className="flex space-x-2"
activeClassName="active"
/> */}
<Pagination count={total} page={page} onChange={handleChange} defaultPage={1} boundaryCount={3} color="secondary" sx={{button:{color:'#ffffff'}}} />
</div>
</div>
</section>
);
};
export default Home;

Issue
The issue with the code/current implementation is that there is only one state.anime.value and one "occupied" value. Each time a movie's "watched" status is toggled the Redux state sets/unsets the single state.anime.value state. In other words, you can toggle N movies "watched" and the state.anime.value value is "occupied", but then toggling just 1 movie back to "unwatched" and state.anime.value is reset back to "". All the Star components read this single state value and this is why the stars all toggle together.
Solution
If you want a specific movie to toggle the Star component when the Eye icon is toggled then I think the solution is a bit simpler than you are making it out to be in your code.
The "eye" and "star" components effectively duplicate your Redux state and don't synchronize well with it. It's often considered a React anti-pattern to duplicate state. The state.anime.favourite and state.anime.watched arrays are all the app needs to know what has been marked "watched" and what has been favourited.
Update the animeSlice to store the anime.mal_id properties in objects instead of arrays to provide O(1) lookup in the UI. Remove the anime.value state as it's unnecessary.
animeSlice.js
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
favourite: {},
watched: {},
page: ""
};
const animeSlice = createSlice({
name: "anime",
initialState,
reducers: {
add_favourite(state, { payload }) {
state.favourite[payload] = true;
},
remove_favourite(state, { payload }) {
delete state.favourite[payload];
},
add_watched(state, { payload }) {
state.watched[payload] = true;
delete state.favourite[payload]; // un-star when adding as watched
},
remove_watched(state, { payload }) {
delete state.watched[payload];
},
pageNumber(state, { payload }) {
state.page = payload;
}
}
});
export const {
add_favourite,
remove_favourite,
add_watched,
remove_watched,
pageNumber
} = animeSlice.actions;
export default animeSlice.reducer;
Update the Eye and Star components to read the true state from the store. These components should pass anime.mal_id to the dispatched actions and use anime.mal_id to check the watched/favourites map objects.
Eye
import { BsEyeSlash, BsEyeFill } from "react-icons/bs";
import { useDispatch, useSelector } from "react-redux";
import { add_watched, remove_watched } from "../features/animeSlice";
const Eye = ({ anime }) => {
const dispatch = useDispatch();
const { watched } = useSelector((state) => state.anime);
const isWatched = watched[anime.mal_id];
const toggle_eye = () => {
dispatch((isWatched ? remove_watched : add_watched)(anime.mal_id));
};
const Eye = isWatched ? BsEyeFill : BsEyeSlash;
const className = [
"text-xl icons_1",
isWatched ? "text-green-500" : "text-red-500"
].join(" ");
return (
<div>
<Eye className={className} onClick={toggle_eye} />
</div>
);
};
export default Eye;
Star
import { AiOutlineStar, AiFillStar } from "react-icons/ai";
import { add_favourite, remove_favourite } from "../features/animeSlice";
import { useDispatch, useSelector } from "react-redux";
const Star = ({ anime }) => {
const { favourite, watched } = useSelector((state) => state.anime);
const dispatch = useDispatch();
const isFavorited = favourite[anime.mal_id];
const isWatched = watched[anime.mal_id];
const toggle_star = () => {
dispatch((isFavorited ? remove_favourite : add_favourite)(anime.mal_id));
};
const Star = isFavorited ? AiFillStar : AiOutlineStar;
const className = [
"text-xl",
isWatched ? "text-gray-300 pointer-events-none" : "text-yellow-500 icon_1"
].join(" ");
return (
<div>
<Star className={className} onClick={toggle_star} />
</div>
);
};
export default Star;

Related

After click on like button the whole website become disappear but after refresh the webpage it increments why does it happens?

I am working with a video application website with a fake json server. Everything is going well but the problem appears while I want to like a video it becomes disappears and throwing an error: Objects are not valid as react child. Why this is happening?
The whole state slice:
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit"
import { getVideo, updateReaction } from "./videoApi"
//initializing the state
const initialState = {
video: {}, //while we are going to push a single video into the state we should put it an object instance...
loading: false,
isError: false,
error: ''
}
// async thunk for getting single video
export const fetchVideo = createAsyncThunk('vidoes/fetchVideo', async (id) => {
const videos = await getVideo(id);
return videos;
});
// another async thunk to react on a single video (like or dislike)
export const fetchReact = createAsyncThunk('video/reaction', async ({ id, reaction }) => {
const videoReaction = await updateReaction({ id, reaction })
return videoReaction;
})
//creating slice
const videoSlice = createSlice({
name: 'video',
initialState,
extraReducers: (builder) => {
builder.addCase(fetchVideo.pending, (state) => {
state.loading = true;
state.isError = false;
})
builder.addCase(fetchVideo.fulfilled, (state, action) => {
state.loading = false;
state.video= action.payload;
})
builder.addCase(fetchVideo.rejected, (state, action) => {
state.loading = false;
state.isError = true;
state.error = action.error?.message;
state.video = [];
})
//writing codes for emplementing like funcitonality
builder.addCase(fetchReact.pending, (state) => {
return state;
})
builder.addCase(fetchReact.fulfilled, (state, action) => {
state.video.likes = action.payload;
state.video.unlikes = action.payload;
})
builder.addCase(fetchReact.rejected, (state) => {
return state;
})
}
});
export default videoSlice.reducer;
This one is video api
import axios from '../../utils/axios'
export const getVideo = async (id) => {
const response = await axios.get(`/videos/${id}`)
return response.data;
}
export const updateReaction = async ({ id, reaction }) => {
console.log(id)
const { data } = await axios.get(`/videos/${id}`)
if (data) {
let updatedReaction =
reaction === 'like'
? {
likes: data.likes + 1,
}
: {
unlikes: data.unlikes + 1,
};
const response = await axios.patch(`/videos/${id}`, updatedReaction);
return response.data;
}
}
Video description component
import LIke from '../Like/LIke';
const VidoeDescription = ({ video }) => {
const { title, date, description, likes, unlikes, id } = video;
return (
<div>
<h1 className="text-lg font-semibold tracking-tight text-slate-800">
{title}
</h1>
<div className="pb-4 flex items-center space-between border-b">
<h2 className="text-sm leading-[1.7142857] text-slate-600 w-full">
Uploaded on {date}
</h2>
<LIke likes={likes} unlikes={unlikes} id={id} />
</div>
<div className="mt-4 text-sm text-[#334155] dark:text-slate-400">
{description}
</div>
</div>
);
};
export default VidoeDescription;
Like component where I put my like and unlike component
import React from 'react';
import { useDispatch } from 'react-redux';
import LikeImg from '../../assets/like.svg';
import UnLikeImg from '../../assets/unlike.svg'
// import { updateReaction } from '../../features/video/videoApi';
import { fetchReact } from '../../features/video/video_slice';
const LIke = ({ likes, unlikes, id }) => {
console.log(id)
const dispatch = useDispatch();
//handle like features
const reactionHandler = ({ id, reaction }) => {
dispatch(fetchReact({ id, reaction }))
}
return (
<div className="flex gap-10 w-48">
<div className="flex gap-1">
<div
className="shrink-0 cursor-pointer"
onClick={() => reactionHandler({ id, reaction: 'like' })}
>
<img className="w-5 block" src={LikeImg} alt="Like" />
</div>
<div className="text-sm leading-[1.7142857] text-slate-600">
{likes >= 1000 ? `${ likes }K` : likes}
</div>
</div>
<div className="flex gap-1">
<div
onClick={() => reactionHandler({ id, reaction: 'unlike' })}
className="shrink-0 cursor-pointer"
>
<img className="w-5 block" src={UnLikeImg} alt="Unlike" />
</div>
<div className="text-sm leading-[1.7142857] text-slate-600">
{unlikes >= 1000 ? `${ unlikes }K` : unlikes}
</div>
</div>
</div>
);
};
export default LIke;
This one is root video page
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import Player from '../component/Player/Player';
import VidoeDescription from '../component/VideoDescription/VidoeDescription';
import { fetchVideo } from '../features/video/video_slice';
import Loading from '../component/Loading/Loading';
import RelatedVideo from '../component/VideoList/RelatedVideo';
const Videos = () => {
const { loading, isError, video, error } = useSelector(state => state.video)
const { id, link, title , tags } = video || {};
const dispatch = useDispatch()
const { videoId } = useParams();
useEffect(() => {
dispatch(fetchVideo(videoId))
}, [dispatch, videoId])
//decide what to render in the ui
let content = null;
if (loading) {
content = <Loading />
}
if (loading && isError) {
content = <div className="col-span-12"><srong>{error}</srong></div>
}
if (!loading && !isError && video?.id) {
content = <div className="col-span-12"><srong>{error}</srong></div>
}
if (!loading && !isError && video?.id) {
content = <div className="mx-auto max-w-7xl px-2 pb-20 min-h-[400px]">
<div className="grid grid-cols-3 gap-2 lg:gap-8">
<div className="col-span-full w-full space-y-8 lg:col-span-2">
<Player link={link} title={title} />
<VidoeDescription video={video} />
</div>
<RelatedVideo currentId={id} tags={tags} />
</div>
</div>
}
return (
<div>
<section className="pt-6 pb-20">
{content}
</section>
</div>
);
};
export default Videos;
Problem
Here:
builder.addCase(fetchReact.fulfilled, (state, action) => {
state.video.likes = action.payload;
state.video.unlikes = action.payload;
})
action.payload looks like this:
{
likes: 1,
}
because it's how you created it in updateReaction
Solution
You wanted to do this instead:
builder.addCase(fetchReact.fulfilled, (state, action) => {
state.video.likes = action.payload.likes;
state.video.unlikes = action.payload.unlikes;
})

display arraystate elements in fragment is not working properly in react js

I have a calendar when the user clicks on a day I get an array with objects of all days with available hours (will put an image of the console.log of it)
then in the useEffect I do some work to get the hours of the day clicked and I want to push the value of date.startAt in the hoursArray state.
the problem is :
1- if there is more than one hour in the same day the last hour is the only one to appear on the screen.
import React, { Component, Fragment, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useEffect } from "react";
import axios from "axios";
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome";
import { faChevronRight, faChevronLeft, } from "#fortawesome/free-solid-svg-icons";
import "./c-timeSlot.css";
import "./c-modal.css";
// import '../../hanaa.css';
import dolar from '../../../pictures/charge.png';
import visa from '../../../pictures/PngItem_1162717.png';
const TimeSlot = (props) => {
const navigate = useNavigate();
console.log('props time slot', props);
const day = props.day;
console.log('slot got ittttttt day', day)
const docId = props.docId;
const token = props.token;
const [availableDates, setAvailableDates] = useState([]);
const hours = [];
const hours2 = [];
const [hoursArray, setHoursArray] = useState(hours);
const [hoursArray2, setHoursArray2] = useState(hours2);
const year = '2022-07-';
var currentDateStr = year.concat(day);
useEffect(() => {
setHoursArray([]);
setHoursArray2([]);
axios.get(`/doctor/${docId}`,
{
headers: {
"token": token
}
}
).then(res => {
console.log('doctor available hours', res);
async function getCalender() {
await setAvailableDates(res.data.calender);
}
getCalender();
console.log('available dates ', availableDates);
}).then(() => {
availableDates.forEach(element => {
console.log('currenttttttttt date', currentDateStr);
console.log(' date', element.date);
async function findHours() {
if (element.date == currentDateStr) {
if (element.startAt.includes('am')) {
await setHoursArray([element.startAt]);
} else {
await setHoursArray2([element.startAt]);
console.log('array fe l b3denn', hoursArray2);
}
}
}
findHours();
})
})
}, [day]);
var sessionType;
return (
<>
<div className="c-bigslots">
<div>
<div className="c-MorningSlots">Morning Hours</div>
<div className="c-slots-up">
{
hoursArray.map((hour, index) => (
<Fragment key={index}>
<div className='c-slot'
>
{hour}
</div>
</Fragment>
))}
</div>
</div>
<div>
<div className="c-EveningSlots">Evening Hours</div>
<div className="c-slots c-slots_evening">
{
hoursArray2.map((hour, index) => (
<Fragment key={index}>
<div className='c-slot'
>
{hour}
</div>
</Fragment>
))}
</div>
</div>
</div>
</>
);
}
export default TimeSlot;
You can append value to existing array in sate as,
async function findHours() {
if (element.date == currentDateStr) {
if (element.startAt.includes('am')) {
setHoursArray(currentHours => [...currentHours, element.startAt]);
} else {
setHoursArray2(currentHours => [...currentHours, element.startAt]);
console.log('array fe l b3denn', hoursArray2);
}
}
........

Click anywhere to close dropdown React

Hi I am creating an app where the user can search for books by title. The user can search and each book result has a dropdown. so I have many dropdowns on a single page (the search results page). I am trying to make a dropdown close when the user clicks outside of the dropdown button (which is a div). Currently I can open the dropdown by clicking on the dropdown button and only close it when clicking on the dropdown button again.
I need the dropdown to close when clicking anywhere except the dropdown options. How would I go about doing this?
ButtonDropDown.js
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { BsFillCaretDownFill } from 'react-icons/bs';
const ButtonDropDown = ({ choices, label }) => {
const [active, setActive] = useState(false);
const toggleClass = () => {
setActive(!active);
};
return (
<div className="dropdown">
<button onClick={toggleClass} type="button" className="dropbtn">
<BsFillCaretDownFill />
</button>
<div
id="myDropdown"
className={`dropdown-content ${active ? `show` : `hide`}`}
>
<div>{label}</div>
{choices.map((choice) => (
<div>{choice}</div>
))}
</div>
</div>
);
};
ButtonDropDown.propTypes = {
choices: PropTypes.arrayOf(PropTypes.string).isRequired,
label: PropTypes.string,
};
ButtonDropDown.defaultProps = {
label: 'Move to...',
};
export default ButtonDropDown;
Book.js
import React from 'react';
import PropTypes from 'prop-types';
import ButtonDropDown from './ButtonDropDown';
const Book = ({ title, authors, thumbnail }) => {
return (
<div className="book">
<img src={thumbnail} alt={title} className="book-thumbnail" />
<div className="book-title">{title}</div>
<div className="book-authors">{authors}</div>
<ButtonDropDown
choices={['Currently Reading', 'Want to Read', 'Read', 'None']}
/>
</div>
);
};
// Move to..., currently reading, want to read, read, none
Book.propTypes = {
thumbnail: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
authors: PropTypes.arrayOf(PropTypes.string),
};
Book.defaultProps = {
authors: [],
};
export default Book;
SearchPage.js
import React, { useEffect, useState } from 'react';
import { BsArrowLeftShort } from 'react-icons/bs';
// import { debounce } from 'debounce';
import SearchBar from '../components/SearchBar';
import { search } from '../api/BooksAPI';
import Book from '../components/Book';
const SearchPage = () => {
const [query, setQuery] = useState('');
const [data, setData] = useState([]);
// const [isLoading, setIsLoading] = useState(true);
const handleChange = (e) => {
setQuery(e.target.value);
};
useEffect(() => {
const bookSearch = setTimeout(() => {
if (query.length > 0) {
search(query).then((res) => {
if (res.length > 0) {
setData(res);
} else setData([]);
});
} else {
setData([]); // make sure data is not undefined
}
}, 1000);
// bookSearch();
// console.log(data); // undefined initially since we didnt search anything
return () => clearTimeout(bookSearch);
// if (data !== []) setIsLoading(false);
// setIsLoading(true);
}, [query]);
return (
<div>
<SearchBar
type="text"
searchValue={query}
placeholder="Search for a book"
icon={<BsArrowLeftShort />}
handleChange={handleChange}
/>
<div className="book-list">
{data !== []
? data.map((book) => (
<Book
key={book.id}
title={book.title}
authors={book.authors}
thumbnail={book.imageLinks.thumbnail}
/>
))
: 'ok'}
</div>
</div>
);
};
export default SearchPage;

Cart funcionality using Context Api

I am trying to achieve cart functionality, but I want to save the cart on the refresh of the page. Anyway, the problem is this I have Context Api and cart logic (add to cart, removeitem, amount, setCartItem) so when I try to add an item to the cart, nothing happens, and when I check the cart nothing is in there. I know this question has a lot of code, and the mistake could be anywhere, if someone could help I would be really grateful.
Cart Context
import React from "react";
function getCartFromLocalStorage() {
return localStorage.getItem("cart")
? JSON.parse(localStorage.getItem("cart"))
: [];
}
const CartContext = React.createContext();
function CartProvider({ children }) {
const [cart, setCart] = React.useState(getCartFromLocalStorage());
const [total, setTotal] = React.useState(0);
const [cartItems, setCartItems] = React.useState(0);
React.useEffect(() => {
localStorage.setItem("cart", JSON.stringify(cart));
let newTotal = cart.reduce((total, cartItem) => {
return (total += cartItem.amount * cartItem.price);
}, 0);
newTotal = parseFloat(newTotal.toFixed(2));
setTotal(newTotal);
let newCartItems = cart.reduce((total, cartItem) => {
return (total += cartItem.amount);
}, 0);
setCartItems(newCartItems);
}, [cart]);
const removeItem = key => {
setCart([...cart].filter(item => item.key !== key));
};
const addToCart = book => {
const { key, image, bookName, by } = book;
const item = [...cart].find(item => item.key === key);
}
const clearCart = () => {
setCart([]);
};
return (
<CartContext.Provider
value={{
cart,
cartItems,
total,
removeItem,
addToCart,
clearCart
}}
>
{children}
</CartContext.Provider>
);
}
export { CartContext, CartProvider };
Books
import React,{useContext} from 'react'
import { CartContext } from '../../context/cart';
import HoverBooks from './HoverBooks';
import { useHistory } from "react-router-dom";
const Books = ({category}) => {
const {addToCart }= useContext(CartContext)
return (
<div className='books__main'>
{category.slice(0, 5).map((book) => {
return(
<>
<HoverBooks
key={book.key}
{...book}
/>
<div className='book__content'>
<li>{book.bookName}</li>
<h4>By{book.by}</h4>
<h4>Narreted by:{book.Narreted}</h4>
<h4>Length: {book.length}</h4>
<h4>Release Date: {book.ReleaseDate}</h4>
<h4>Language: {book.Language}</h4>
<h4>{book.rating}</h4>
</div>
<div>
<span>Regular Price: {book.RegularPrice}</span>
<button onClick={() => {
addToCart(book);
}}
>Add to cart</button>
</div>
</>
)})}
</div>
)
}
export default Books
Cart
import React from "react";
import { Link } from "react-router-dom";
import EmptyCart from "./EmptyCart";
import CartItem from './CartItem'
import { UserContext } from "../../context/user";
import { CartContext } from "../../context/cart";
import './Cart.css'
export default function Cart() {
const { cart, total } = React.useContext(CartContext);
const { user } = React.useContext(UserContext);
if (cart.length === 0) {
return <EmptyCart />;
}
return (
<div className="cart__items">
<h2>your cart</h2>
{cart.map(item => {
return <CartItem key={item.key} {...item} />;
})}
<h2>total : ${total}</h2>
{user.token ? (
<Link to="/cart" className="btn">
</Link>
) : (
<Link to="/login" className="btn">
login
</Link>
)}
</div>
);
}
Cart Item
import React, { useContext } from "react";
import { CartContext } from "../../context/cart";
export default function CartItem({ key, image,bookName, amount }) {
const {removeItem} = useContext(CartContext)
return (
<div className="cart__item">
<img src={image} />
<div>
<h4>{bookName}</h4>
<h5>${by}</h5>
<button
className="cart__removebtn"
onClick={() => {
removeItem(key);
}}
>
remove
</button>
</div>
<p className="item__amount">{amount}</p>
</div>
);
}
Cart Link
import React from "react";
import { Link } from "react-router-dom";
import {FiShoppingCart} from 'react-icons/fi'
import { CartContext } from "../../context/cart";
export default function CartLink() {
const { cartItems } = React.useContext(CartContext);
return (
<div className="cartlink__container">
<Link to="/cart">
<FiShoppingCart />
</Link>
<span className="cartlink__total">{cartItems}</span>
</div>
);
}
Home
import React,{useState, useEffect, useContext} from 'react'
import './Home.css'
import Books from './Books'
import { BookContext } from "../../context/books";
const Home = () => {
const {data, handleSelectCategory, currentSelectedCategory }
=useContext(BookContext)
return (
<div className='books__container' >
<h1 className='categories'>Categories</h1>
{Object.keys(data).map((key, index)=>{
let books = data[key];
return (
<>
<span key={key} onClick={() => handleSelectCategory(key)}
className='books__list' >
{books[0].category}
</span>
</>
);})}
<Books category={currentSelectedCategory} />
</div>
)
}
export default Home
book
import React, {useState, useEffect} from 'react'
import URL from '../utilis/URL';
const BookContext = React.createContext();
export default function BooksProvider({ children }) {
const [data, setData] = useState([])
const [currentSelectedCategory, setCurrentSelectedCategory] = useState([]);
const handleSelectCategory = (category) => {
setCurrentSelectedCategory(data[category]);
};
const fetchData = async () => {
const response = await fetch(URL);
const result = await response.json();
console.log(result)
setCurrentSelectedCategory(result[Object.keys(result)[0]]);
setData(result);
};
useEffect(()=>{
fetchData();
},[])
return (
<BookContext.Provider value={{ data, handleSelectCategory, setCurrentSelectedCategory, currentSelectedCategory }}>
{children}
</BookContext.Provider>
);
}
export {BookContext, BooksProvider}
function onUpdate() {
localStorage.setItem("cart", JSON.stringify(cart));
let newTotal = cart.reduce((total, cartItem) => {
return (total += cartItem.amount * cartItem.price);
}, 0);
newTotal = parseFloat(newTotal.toFixed(2));
setTotal(newTotal);
let newCartItems = cart.reduce((total, cartItem) => {
return (total += cartItem.amount);
}, 0);
setCartItems(newCartItems);
}
React.useEffect(onUpdate, [cart]);
const addToCart = (book) => {
const { key, image, bookName, by } = book;
let item = cart.find((item) => item.key === key);
if (item) {
item.amount++;
onUpdate();
} else {
setCart(
cart.concat({
amount: 1,
price: book.RegularPrice,
// add other cartItem attributes.
...book
})
);
}
};

Why react loads images for many times? is it possible to save the first load?

to watch the problem you can vivsit the test site http://u100525.test-handyhost.ru/products
the problem appears if to click many times on category items, images of products start to bug becouse react loads image of one item over and over again, on every change of category - on every filter of products, so how to make one load and save somehow the loaded images?
so if i click on categories my code is filtering products array and update statement - visibleProducts then im doing visibleProducts.map((product)=>{});
and i`m getting bug problem, because every time when react renders my the component does request to the server for getting image by id and waits while the image will load, but if i click on an other category react(ProductItem) starts other request for new images then it is starting to bug they start blinking and changing ;c
im new in react and just stated to practice what i have to do guys?
is my code correct ?
here is my ProductItem component ->
import React, { useState, useEffect, memo, useCallback } from "react";
import { Link } from "react-router-dom";
import { connect } from "react-redux";
import { setModalShow, onQuickViewed, addedToCart } from "../../actions";
import Checked from "../checked";
import "./product-item.css";
import Spinner from "../spinner";
const ProductItem = ({
product,
wpApi,
addedToCart,
onQuickViewed,
setModalShow,
}) => {
const [prodImg, setProdImg] = useState("");
const [animated, setAnimated] = useState(false);
const [checked, setChecked] = useState(false);
const [itemLoading, setItemLoading] = useState(true);
const checkedFn = useCallback(() => {
setChecked(true);
setTimeout(() => {
setChecked(false);
}, 800);
},[product]);
const onModalOpen = useCallback((e, id) => {
onQuickViewed(e, id);
setModalShow(true);
}, product);
const addHandle = useCallback((e, id) => {
e.preventDefault();
addedToCart(id);
checkedFn();
},[product]);
useEffect(()=>{
setItemLoading(false);
}, [prodImg]);
useEffect(() => {
wpApi.getImageUrl(product.imageId).then((res) => {
setProdImg(res);
});
});
return (
<div className="product foo">
<div
className='product__inner'}
>
{!itemLoading? <div
className="pro__thumb"
style={{
backgroundImage:prodImg
? `url(${prodImg})`
: "assets/images/product/6.png",
}}
>
<Link
to={`/product-details/${product.id}`}
style={{ display: `block`, width: `100%`, paddingBottom: `100%` }}
>
</Link>
</div>: <Spinner/>}
<div className="product__hover__info">
<ul className="product__action">
<li>
<a
onClick={(e) => {
onModalOpen(e, product.id);
}}
title="Quick View"
className="quick-view modal-view detail-link"
href="#"
>
<span ><i class="zmdi zmdi-eye"></i></span>
</a>
</li>
<li>
<a
title="Add TO Cart"
href="#"
onClick={(e) => {
addHandle(e, product.id);
}}
>
{checked ? (
<Checked />
) : (
<span className="ti-shopping-cart"></span>
)}
</a>
</li>
</ul>
</div>
</div>
<div className="product__details">
<h2>
<Link to={`/product-details/${product.id}`}>{product.title}</Link>
</h2>
<ul className="product__price">
<li className="old__price">${product.price}</li>
</ul>
</div>
</div>
);
};
const mapStateToProps = ({ options, cart, total, showModal }) => {
return {};
};
const mapDispatchToProps = {
onQuickViewed,
setModalShow,
addedToCart,
};
export default connect(mapStateToProps, mapDispatchToProps)(memo(ProductItem));
here is my parent component Products ->
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import ProductItem from "../product-item";
import { withWpApiService } from "../hoc";
import { onQuickViewed, addedToCart, categoriesLoaded } from "../../actions";
import CategoryFilter from "../category-filter";
import Spinner from "../spinner";
import "./products.css";
const Products = ({
maxProducts,
WpApiService,
categoriesLoaded,
addedToCart,
onQuickViewed,
products,
categories,
loading,
}) => {
const [activeIndex, setActiveIndex] = useState(0);
const [activeCategory, setActiveCategory] = useState(0);
const [visibleProducts, setVisibleProducts] = useState([]);
const wpApi = new WpApiService();
useEffect(() => {
updateVisibleProducts(activeCategory, products);
}, [products]);
useEffect(() => {
wpApi.getCategories().then((res) => {
categoriesLoaded(res);
});
}, []);
const getCatId = (cat) => {
setActiveCategory(cat);
updateVisibleProducts(cat, products);
setActiveIndex(cat);
};
const updateVisibleProducts = (category, products) => {
let updatedProducts = [];
switch (category) {
case 0:
updatedProducts = products;
setVisibleProducts(updatedProducts);
break;
default:
updatedProducts = products.filter(
(product) => product.categories.indexOf(category) >= 0
);
setVisibleProducts(updatedProducts);
}
};
let currentLocation = window.location.href.split("/");
if (!loading) {
return (
<section className="htc__product__area shop__page mb--60 mt--130 bg__white">
<div className={currentLocation[3] == "" ? `container` : ""}>
<div className="htc__product__container">
<CategoryFilter
activeIndex={activeIndex}
categories={categories}
getCatId={getCatId}
/>
<div
className="product__list another-product-style"
style={{ height: "auto" }}
>
{visibleProducts
.slice(0, maxProducts ? maxProducts : products.length)
.map((prod, id) => {
return (
<ProductItem
wpApi={wpApi}
key={id}
onQuickViewed={onQuickViewed}
addedToCart={addedToCart}
product={prod}
/>
);
})}
</div>
</div>
</div>
</section>
);
} else {
return <Spinner />;
}
};
const mapStateToProps = ({ products, loading, activeCategory, categories }) => {
return {
products,
activeCategory,
categories,
loading,
};
};
const mapDispatchToProps = {
addedToCart,
categoriesLoaded,
onQuickViewed,
};
export default withWpApiService()(
connect(mapStateToProps, mapDispatchToProps)(Products)
);
and if you need, here is my CategoryFilter component ->
import React from 'react'
const CategoryFilter = ({categories, getCatId, activeIndex}) => {
return (
<div className="row mb--60">
<div className="col-md-12">
<div className="filter__menu__container">
<div className="product__menu">
{categories.map((cat) => {
return (
<button key={cat.id}
className={activeIndex === cat.id? 'is-checked' : null}
onClick={() => getCatId(cat.id)}
data-filter=".cat--4"
>
{cat.name}
</button>
);
})}
</div>
</div>
</div>
</div>
)
}
export default CategoryFilter

Categories

Resources