React - Prevent re-render whole list when delete element - reactjs

I'm working on a toasts notifications system using React v17 and the React context API. I'm NOT using Redux.
The problem:
Toasts are dismissed automatically after a given delay. The toast element which is dismissed is removed from the list from the context. The problem is that each Toast component is re-render, the whole list is re-render, each time the list change.
I don't want that each component be re-rendered. Only the dismissed Toast component should be "re-render", understand deleted from the list displayed.
I put key attribute on my Toast components but it doesn't work as I expected it would.
Thank you for helping me !
The code below:
function Layout() {
const toastsContext = useContext(ToastsContext);
const toastsList = toastsContext.toastsList;
const [list, setList] = useState([]);
useEffect(() => {
setList(toastsList);
}, [toastsList]);
const displayToasts = list.map(toast =>
<Toast
key={toast.id.toString()}
id={toast.id}
color={toast.color}
title={toast.title}
message={toast.message}
dismissable={toast.dismissable}
showTime={toast.showTime}
autoDismissDelay={toast.autoDismissDelay}
redirectTo={toast.redirectTo} />
);
return(
<div className='bg-slate-800 text-slate-400 min-h-screen relative'>
<Header />
<Outlet />
<div className='fixed top-20 right-4 flex flex-col gap-2'>
{displayToasts}
</div>
</div>
);
}
export default Layout;
Toast component
import { memo, useCallback, useContext, useEffect, useState } from "react";
import { ToastsContext } from "../context/ToastsContext";
import { FiX, FiArrowRight } from 'react-icons/fi';
import { Link } from "react-router-dom";
function Toast({id, color, title, message, dismissable, autoDismissDelay, showTime, redirectTo}) {
const toastsContext = useContext(ToastsContext);
const [hiddenToast, setHiddenToast] = useState(false);
const getToastColor = () => {
switch (color) {
case 'primary':
return 'bg-sky-500';
case 'danger':
return 'bg-rose-500';
case 'success':
return 'bg-green-500';
default:
return 'bg-sky-500';
}
};
const dismissToast = useCallback(() => {
setHiddenToast(true);
setTimeout(() => {
document.getElementById(`toast${id}`).className = 'hidden';
toastsContext.dismissToast(id);
}, 310);
}, [id, toastsContext]);
useEffect(() => {
const interval = setInterval(() => {
dismissToast();
}, autoDismissDelay);
return () => {
clearInterval(interval);
}
}, [autoDismissDelay, dismissToast]);
return(
<div id={'toast'+id} className={`transition ease-out duration-300 text-slate-50 text-sm rounded-lg drop-shadow-lg opacity-100 ${getToastColor()} ${hiddenToast ? 'translate-x-20 opacity-0' : ''}`}>
<div className="w-72">
<div className={`${(redirectTo && redirectTo !== '') || (message && message !== '') ? 'py-2' : 'py-4'} px-4 flex font-semibold items-center`}>
<p>{title}</p>
<div className="ml-auto flex items-center">
{showTime ? <p className="text-xs mr-3">11m ago</p> : null}
{dismissable ?
<button type="button" className="flex items-center justify-center text-base" onClick={dismissToast}>
<FiX />
</button>
: null
}
</div>
</div>
{
redirectTo && redirectTo !== '' ?
<Link className={`border-t border-slate-700 bg-slate-800 rounded-b-lg px-4 py-3 font-medium hover:bg-slate-700 block`} to={redirectTo}>
<p className="flex items-center justify-between">
<span>{message ? message : 'See more'}</span>
<FiArrowRight />
</p>
</Link>
:
message ?
<div className={`border-t border-slate-700 bg-slate-800 rounded-b-lg px-4 py-3 font-medium`}>
<p className="flex items-center justify-between">
<span>{message}</span>
</p>
</div>
: null
}
</div>
</div>
);
};
export default memo(Toast);

That's the default behavior in react: When a component (eg, Layout) re renders, so to do all its children (the Toasts). If you want to skip rendering some of the toasts, then Toast will need to use React.memo. Also, for the memoization to work, the props to each Toast will need to stay the same from one render to the next. From looking at your code i think that will happen without any changes, but it's important to know so you don't think memo is enough on its own.
import { memo } from 'react';
function Toast() {
// ...
}
export default memo(Toast);

Related

What is the best way to replace id in react?

I am working on a menu component. And with the help of the id, I made it possible for it to open and close on a click outside the component.
But I understand that I'm doing something wrong. If I open other menus, then the previous one does not close, I tried to pass props to override the id, but they are unstable (in fact, they stopped working altogether, but I didn’t dive further and look for an answer why)
I understand that this is not best practice. The component must be able to be reused. So I think how to improve it, should I use refs? Hook onChange? Which method do you think is more convenient, more practical?
import { useState } from "react";
function OverflowMenu(props) {
const [isActive, setIsActive] = useState(false);
const handleClick = (event) => {
setIsActive((current) => !current);
document.addEventListener("click", (event) => {
const target = event.target;
if (!target.closest(`#wrapper`) && !target.closest(`#menu`)) {
setIsActive(false);
}
});
};
return (
<>
<div
id={"wrapper"}
onClick={handleClick}
className="relative flex cursor-pointer"
>
{props.children} {props.content}
<div
id={"menu"}
className={`${!isActive && "hidden"}
${props.topRight && "-right-0 bottom-full"}
${props.topLeft && "-left-0 bottom-full"}
${props.bottomRight && "-right-0 top-full"}
${props.bottomLeft && "-left-0 top-full"}
absolute z-20 my-[4px] flex min-w-[112px] max-w-[280px] flex-col`}
>
<div className="scroll flex max-h-[310px] min-w-max flex-col overflow-hidden overflow-y-auto rounded-[8px] bg-blue-50 py-[8px] shadow-mm-1 dark:bg-gray-800 ">
<>{props.menu}</>
</div>
</div>
</div>
</>
);
}
export { OverflowMenu };
Hi #sllmn,
There are a few ways you could improve the current implementation.
One option would be to use a ref hook. You could use that for passing the reference of HTML element.
const menuRef = useRef(null)
const handleClick = (event) => {
setIsActive((current) => !current)
if (!menuRef.current.contains(event.target)) {
setIsActive(false)
}
}
// code....
<div
id={'menu'}
ref={menuRef}
className={""}>
// code....
</div>
Another option. Which I found on internet, and it's a good one. To use useOnClickOutside hook. You will need to install it first.
yarn add react-use
Now you can use by importing that library.
import { useOnClickOutside } from 'react-use';
import { useState, useRef } from 'react';
import { useOnClickOutside } from 'react-use';
function OverflowMenu(props) {
const [isActive, setIsActive] = useState(false);
const menuRef = useRef(null);
useOnClickOutside(menuRef, () => setIsActive(false));
const handleClick = (event) => {
setIsActive((current) => !current);
};
return (
<>
<div
id={"wrapper"}
onClick={handleClick}
className="relative flex cursor-pointer"
>
{props.children} {props.content}
<div
id={"menu"}
ref={menuRef}
className={`${!isActive && "hidden"}
${props.topRight && "-right-0 bottom-full"}
${props.topLeft && "-left-0 bottom-full"}
${props.bottomRight && "-right-0 top-full"}
${props.bottomLeft && "-left-0 top-full"}
absolute z-20 my-[4px] flex min-w-[112px] max-w-[280px] flex-col`}
>
<div className="scroll flex max-h-[310px] min-w-max flex-col overflow-hidden overflow-y-auto rounded-[8px] bg-blue-50 py-[8px] shadow-mm-1 dark:bg-gray-800 ">
<>{props.menu}</>
</div>
</div>
</div>
</>
);
}
export { OverflowMenu };

SlateJS React trying to get a textfield to work inside an Element

I have a Slate element that looks like this:
import React, {useState} from 'react'
import "./App.css"
export default function Accordion(props) {
const [closed, setClosed] = useState(false) //todo eventually fetch starting position from savefile
const [heightClass, setHeightClass] = useState("")
const handleToggle = () => {
if(closed === false){
setHeightClass("h-0")
}
else{
setHeightClass("")
}
setClosed(!closed)
}
return (
<>
<div {...props.attributes}>
{/* title and button */}
<div className='flex justify-between '>
<div className='font-semibold'>
<DefaultElement {...props}/> //the title of the accordion
</div>
<div
className={`px-2 cursor-pointer font-bold font-mono select-none ${closed ? "rotate-180" : ""}`}
onClick={() => {
handleToggle()
}}>
V
</div>
</div>
{/* ${closed ? "h-0" : ""} */}
<div className={`rounded border-l px-2 overflow-hidden accordionTransition `}
style={{height: closed ? "0px" : ""}}>
{props.children}
</div>
</div>
</>
)
}
const DefaultElement = props => {
console.log(props)
return <p {...props.attributes}>
{props.children}
</p>
}
Which is used by the Editor in App.js:
const App = () => {
const [editor] = useState(() => withReact(createEditor()))
// Define a rendering function based on the element passed to `props`. We use
// `useCallback` here to memoize the function for subsequent renders.
const renderElement = useCallback(props => {
switch (props.element.type) {
case 'accordion':
return <Accordion {...props} />
default:
return <DefaultElement {...props} />
}
}, [])
return (
<div className='p-5'>
<Slate editor={editor} value={initialValue}>
<Editable
// Pass in the `renderElement` function.
renderElement={renderElement}
/>
</Slate>
</div>
)
}
const DefaultElement = props => {
return <span {...props.attributes}>
{props.children}
</span>
}
export default App;
I'm trying to get the Accordion title to be properly editable and work as Slate intended. Just a a simple textfield. I can't figure out the correct syntax and how i'm supposed to do this.
When i do this, the title and the accordion content (props.children) are the same. I don't understand why.
If i remove the <DefaultElement {...props}/> and just write some text, it throws me this error when i edit it: Uncaught Error: Cannot resolve a Slate point from DOM point: [object Text],4

Lazy loading element in typescript hook not working

I've implemented a lazy loading hook, but for some reason the carousel component is not loading at all. I've got it from here: https://betterprogramming.pub/lazy-loading-in-next-js-simplified-435681afb18a
This is my child that I want to render when it's in view:
import { useRef } from "react";
import Carousel from "../components/carousel";
import useOnScreen from "../hooks/useOnScreen";
// home page
function HomePage() {
const carouselRef = useRef();
const carouselRefValue = useOnScreen(carouselRef);
return (
<div className="snap-y">
{/* 1 */}
<div className="flex justify-center items-start relative min-h-[calc(100vh_-_5rem)] bg-black snap-start ">
{/* Cone */}
<div className="absolute w-full max-w-full overflow-hidden min-w-fit cone animate-wiggle"></div>
<div className="grid justify-center grid-cols-4 max-w-7xl">
//Content
</div>
{/* 2 */}
<div className="z-20 pb-4 bg-black snap-start" ref={carouselRef.current}>
{carouselRefValue && <Carousel />}
</div>
{/* 3 */}
<div className="flex items-start justify-center py-32 min-h-fit bg-slate-50 snap-start">
//More content
</div>
);
}
export default HomePage;
useOnScreen hook:
import { useState, useEffect } from "react";
const useOnScreen = (ref: any) => {
const [isIntersecting, setIntersecting] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => setIntersecting(entry.isIntersecting));
if (ref.current) {
observer.observe(ref.current);
}
}, []);
return isIntersecting;
};
export default useOnScreen;
Edit: Needed to add const carouselRef = useRef() as React.MutableRefObject<HTMLInputElement>; paired with #asynts's answer.
This is incorrect:
<div className="z-20 pb-4 bg-black snap-start" ref={carouselRef.current}></div>
it should be:
<div className="z-20 pb-4 bg-black snap-start" ref={carouselRef}></div>

Nextjs SSG Suspense throws Hydration Error

I've got a static site and I'm trying to render a simple list of items retrieved from Supabase. I had this working in React 17, but with React 18 it throws this error sporadically, and the fallback doesn't reliably appear. This seems to mostly happen when doing a hard page refresh. Authentication is done via cookie and server-side middleware.
Error: This Suspense boundary received an update before it finished hydrating. This caused the boundary to switch to client rendering. The usual way to fix this is to wrap the original update in startTransition.
//index.tsx (page component)
import { AddEventButton } from '#/components/index';
import { ComponentWithLayout } from '#/types/definitions';
import { Suspense } from 'react';
import { getNavbarLayout } from '#/layouts/NavbarLayout';
import UpcomingEventListSkeleton from '#/components/upcomingEventList/UpcomingEventList.skeleton';
import dynamic from 'next/dynamic';
const UpcomingEventList = dynamic(
() => import('#/components/upcomingEventList/UpcomingEventList'),
{ suspense: true }
);
const UpcomingEventsPage: ComponentWithLayout = () => {
return (
<div className="mx-auto lg:w-1/2 xs:w-full">
<div className="space-y-2">
<Suspense fallback={<UpcomingEventListSkeleton />}>
<UpcomingEventList />
</Suspense>
</div>
<AddEventButton />
</div>
);
};
UpcomingEventsPage.getLayout = getNavbarLayout;
export default UpcomingEventsPage;
//UpcomingEventList.tsx
import { CalendarEvent } from '#/types/definitions';
import { CalendarEventConfiguration } from '#/utils/appConfig';
import { UpcomingEvent } from './components/upcomingEvent/UpcomingEvent';
import { supabaseClient } from '#supabase/auth-helpers-nextjs';
import React from 'react';
import dayjs from 'dayjs';
import useSWR from 'swr';
/**
* Shows a list of Upcoming Event components
* #returns UpcomingEventList component
*/
export const UpcomingEventList = () => {
const { data } = useSWR(
'upcomingEvents',
async () =>
await supabaseClient
.from<CalendarEvent>(CalendarEventConfiguration.database.tableName)
.select('*, owner: user_profile(full_name, avatar)')
.limit(10)
.order('end', { ascending: true })
.gt('end', dayjs(new Date()).toISOString()),
{
refreshInterval: 3000,
suspense: true,
}
);
return (
<>
{data && data.data?.length === 0 && (
<div className="card shadow bg-base-100 mx-auto w-full border text-center p-5">
<div className="text-base-content font-medium">
No upcoming events scheduled
</div>
<div className="text-sm opacity-50">
Events can be scheduled by clicking the purple button in the
bottom-right corner.
</div>
</div>
)}
{data &&
data.data?.map(
({ id, owner, start, end, number_of_guests, privacy_requested }) => (
<UpcomingEvent
key={id}
title={owner?.full_name}
startDate={start}
endDate={end}
numberOfGuests={number_of_guests}
privacyRequested={privacy_requested}
avatarSrc={owner?.avatar}
/>
)
)}
</>
);
};
export default UpcomingEventList;
//UpcomingEventListSkeleton.tsx
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import {
faEye,
faPlayCircle,
faStopCircle,
faUser,
} from '#fortawesome/free-solid-svg-icons';
import React from 'react';
const UpcomingEventListSkeleton = () => (
<>
{[...Array(5).keys()].map((v, i) => (
<UpcomingEventSkeleton key={i} />
))}
</>
);
export default UpcomingEventListSkeleton;
const UpcomingEventSkeleton = () => (
<div className="card shadow bg-base-100 text-base-content font-medium mx-auto w-full border">
<div className="flex items-center m-2 animate-pulse">
<div className="mr-5 ml-1">
<div className="rounded-full bg-gray-200 h-12 w-12"></div>
</div>
<span className="space-y-1">
<div className="text-xl">
<div className="h-8 bg-gray-200 rounded w-56"></div>
</div>
<div
className="text-sm opacity-50"
style={{
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
width: '20em',
}}
>
<div className="grid grid-cols-10 grid-rows-4">
<div>
<FontAwesomeIcon icon={faPlayCircle} className="col-span-1" />
</div>
<div className="h-2 bg-gray-200 rounded col-span-9 mt-1 w-36"></div>
<div>
<FontAwesomeIcon icon={faStopCircle} />
</div>
<div className="h-2 bg-gray-200 rounded col-span-9 mt-1 w-36"></div>
<div>
<FontAwesomeIcon icon={faUser} />
</div>
<div className="h-2 bg-gray-200 rounded col-span-9 mt-1 w-36"></div>
<div>
<FontAwesomeIcon icon={faEye} />
</div>
<div className="h-2 bg-gray-200 rounded col-span-9 mt-1 w-36"></div>
</div>
</div>
</span>
</div>
</div>
);
I've tried swapping the components around, storing the results via useEffect to force client-side rendering, and nothing seems to make this error go away or even change behavior. A simple example that removes the custom components and replaces them with simple strings suffers the exact same issue.
I don't understand how useTransition would help me here, nor how I would even make use of it with useSWR as my fetching mechanism. I've searched around and most people who have this issue seem to be using SSR, and not SSG as I am.
Any help is appreciated.
The cause of my issue was useSwr was making calls before the #supabase/auth-helpers-nextjs user object had fully initialized. This was causing multiple requests to be fired, some authorized, others not, and was making the data object flip rapidly between multiple null | hydrated states. Once I added a conditional to useSwr, it stopped the calls and Suspense began working correctly.
import { CalendarEvent } from '#/types/definitions';
import { CalendarEventConfiguration } from '#/utils/appConfig';
import { UpcomingEvent } from './components/upcomingEvent/UpcomingEvent';
import { supabaseClient } from '#supabase/auth-helpers-nextjs';
import { useUser } from '#supabase/auth-helpers-react';
import React from 'react';
import dayjs from 'dayjs';
import useSWR from 'swr';
/**
* Shows a list of Upcoming Event components
* #returns UpcomingEventList component
*/
export const UpcomingEventList = () => {
const user = useUser(); // <--- get user via hook
const { data } = useSWR(
user && !user.isLoading ? 'upcomingEventsList' : null, // <--- make SWR conditional on user being available
async () =>
await supabaseClient
.from<CalendarEvent>(CalendarEventConfiguration.database.tableName)
.select('*, owner: user_profile(full_name, avatar)')
.limit(10)
.order('end', { ascending: true })
.gt('end', dayjs(new Date()).toISOString()),
{ refreshInterval: 10000, suspense: true }
);
return (
<>
{data?.data && !data.data.length && (
<div className="card shadow bg-base-100 mx-auto w-full border text-center p-5">
<div className="text-base-content font-medium">
No upcoming events scheduled
</div>
<div className="text-sm opacity-50">
Events can be scheduled by clicking the purple button in the
bottom-right corner.
</div>
</div>
)}
{data?.data?.map(
({ id, owner, start, end, number_of_guests, privacy_requested }) => (
<UpcomingEvent
key={id}
title={owner?.full_name}
startDate={start}
endDate={end}
numberOfGuests={number_of_guests}
privacyRequested={privacy_requested}
avatarSrc={owner?.avatar}
/>
)
)}
</>
);
};
export default UpcomingEventList;
This worked in my case, but I'd wager that other encountering this error may have similar request patterns that cause Suspense to perform poorly, or break altogether.
SWR was somewhat masking the issue, and by removing and testing the calls with useState and useEffect, I was able to narrow down the problem to the SWR call, and finally the missing user object.

Make only one post request with new state after clicking on one of multiple identical components

I have a small issue and do not have a good idea how to solve it. Hope you can help
I have created a simple Starrating component. You have five stars. If you click on one of the stars the state changes and so on ... (5 stars. Rating from 1 to 5 :D ). Just basic stuff.
The main problem is based on the fact that the Starrating component is a part of another component (AlbumList.js), which is rendered 5 times on the homepage (5 different pictures which you can rate)
(between there is another component AlbumCard.js which is holding the Starrating component but I assume that's not important.
Basically I have 5 components which are the same and each one of them has the Starrating Component.
My main goal is to click on one of the pictures, rate that and send the right state to my database.
The function which is sending the right rating to the database (rateAlbum), is invoked in useEffect, because only there I am able to send the new state to my database(
outside useEffect I only have access to the new state after rendering, I guess).
Unfortunately if I reload the page or just make one rate the function is invoked as many times as pictures there are (5 times)
How do I call the function just ones if I just rated one picture or just after the onClick on the right picture with the new state ?
Starrating.js
import React, { useEffect, useState } from 'react'
import { FaStar } from 'react-icons/fa'
import { rateAlbum } from '../../store/actions/userAlbumRatingAction'
function Starrating({ width }) {
const [rating, setRating] = useState(null)
const [hover, setHover] = useState(null)
const ratePicture = (rating) => {
setRating(rating)
// ratePicture() do not have the new state of rating
}
useEffect(() => {
rateAlbum({ // function which is making the axios call
//... not imporant information just the right IDs and so on
rating: rating,
})
}, [rating])
return (
<div className='flex h-full' style={{ width: width }}>
{[...Array(5)].map((star, i) => {
const ratinValue = i + 1
return (
<label key={ratinValue} className='flex items-center w-full'>
<input
className='hidden'
type='radio'
name='raiting'
value={ratinValue}
onClick={
() => ratePicture(ratinValue)
// () => setRating(ratinValue)
}
/>
<FaStar
className='md:m-1 w-full h-full delay-200 cursor-pointer'
color={ratinValue <= (hover || rating) ? '#ffc107' : '#e4e5e9'}
onMouseEnter={() => setHover(ratinValue)}
onMouseLeave={() => setHover(null)}
/>
</label>
)
})}
</div>
)
}
export default Starrating
AlbumList.js (Starrting component is a part of the AlbumCard Component)
import { connect, useSelector } from 'react-redux'
import { fetchAlbum } from '../../store/actions/albumAction'
import AlbumCard from './AlbumCard'
import { setView } from '../../store/actions/uiAction'
import { useHistory } from 'react-router'
function AlbumList(props) {
const newreleases = useSelector((state) => state.newReleases.NewReleases)
const view = useSelector((state) => state.ui.view)
const searchAlbum = useSelector((state) => state.search.albums)
const history = useHistory()
const onAlbumCardClick = (dataId) => {
props.fetchAlbum(dataId)
history.push('/home/album')
}
return (
<section className='sm:flex sm:justify-between sm:flex-nowrap grid grid-cols-3'>
{view === 'noSearch' ? (
<>
{newreleases.slice(0, 5).map((data) => (
<AlbumCard
url={data.images[0].url}
key={data.id}
id={data.id}
albumname={data.name}
onClick={() => onAlbumCardClick(data.id)}
/>
))}
</>
) : view === 'search' ? (
<>
{searchAlbum.slice(0, 5).map((data, index) => (
<AlbumCard
url={data.images[0].url}
key={data.id}
id={data.id}
albumname={data.name}
onClick={() => onAlbumCardClick(data.id)}
/>
))}
</>
) : null}
</section>
)
}
const mapDispatch = { fetchAlbum, setView }
export default connect(null, mapDispatch)(AlbumList)
rateAlbum function
export const rateAlbum = (data) => {
axios.post('....', data)
}
AlbumCard.js ( not important, but has the Starrting component and Albumcard.js is part of
ALbumList.js)
import React from 'react'
import CardButtons from './CardButtons'
import Starrating from '../HelperComponents/Starrating'
function AlbumCard({ url, albumname, onClick }) {
return (
<>
<div className=' sm:m-2 sm:w-40 dark:bg-white w-24 m-1 rounded-lg shadow-md'>
<div onClick={onClick} id='hi' className='group relative rounded-lg'>
<img
className='md:w-72 block w-full h-full rounded-lg'
src={url}
alt=''
/>
<div className='group-hover:bg-opacity-60 group-hover:opacity-100 justify-evenly absolute top-0 flex items-center w-full h-full transition bg-black bg-opacity-0 rounded-md'>
<CardButtons />
</div>
</div>
<div className=' flex flex-col items-center justify-center pt-3 pb-3'>
<p className='dark:text-black font-body whitespace-nowrap flex justify-center w-11/12 mb-2 overflow-hidden text-xs text-black'>
{albumname}
</p>
<Starrating />
</div>
</div>
</>
)
}
export default AlbumCard
Startrating.js
import React, { useEffect, useState } from 'react'
import { FaStar } from 'react-icons/fa'
import { rateAlbum } from '../../store/actions/userAlbumRatingAction'
function Starrating({ width, onClickStar }) {
const [rating, setRating] = useState(null)
const [hover, setHover] = useState(null)
const ratePicture = (rating) => {
setRating(rating)
// ratePicture() do not have the new state of rating
}
useEffect(() => {
rateAlbum({ // function which is making the axios call
//... not imporant information just the right IDs and so on
rating: rating,
})
}, [rating])
return (
<div className='flex h-full' style={{ width: width }}>
{[...Array(5)].map((star, i) => {
const ratinValue = i + 1
return (
<label key={ratinValue} className='flex items-center w-full'>
<input
className='hidden'
type='radio'
name='raiting'
value={ratinValue}
onClick={
() => onClickStart(ratinValue)
// () => setRating(ratinValue)
}
/>
<FaStar
className='md:m-1 w-full h-full delay-200 cursor-pointer'
color={ratinValue <= (hover || rating) ? '#ffc107' : '#e4e5e9'}
onMouseEnter={() => setHover(ratinValue)}
onMouseLeave={() => setHover(null)}
/>
</label>
)
})}
</div>
)
}
export default Starrating
AlbumCard,js
import React from 'react'
import CardButtons from './CardButtons'
import Starrating from '../HelperComponents/Starrating'
function AlbumCard({ url, albumname, onClick, onClickStar }) {
return (
<>
<div className=' sm:m-2 sm:w-40 dark:bg-white w-24 m-1 rounded-lg shadow-md'>
<div onClick={onClick} id='hi' className='group relative rounded-lg'>
<img
className='md:w-72 block w-full h-full rounded-lg'
src={url}
alt=''
/>
<div className='group-hover:bg-opacity-60 group-hover:opacity-100 justify-evenly absolute top-0 flex items-center w-full h-full transition bg-black bg-opacity-0 rounded-md'>
<CardButtons />
</div>
</div>
<div className=' flex flex-col items-center justify-center pt-3 pb-3'>
<p className='dark:text-black font-body whitespace-nowrap flex justify-center w-11/12 mb-2 overflow-hidden text-xs text-black'>
{albumname}
</p>
<Starrating onClickStar={onClickStar} />
</div>
</div>
</>
)
}
export default AlbumCard
AlbumList.js
import { connect, useSelector } from 'react-redux'
import { fetchAlbum } from '../../store/actions/albumAction'
import AlbumCard from './AlbumCard'
import { setView } from '../../store/actions/uiAction'
import { useHistory } from 'react-router'
function AlbumList(props) {
const newreleases = useSelector((state) => state.newReleases.NewReleases)
const view = useSelector((state) => state.ui.view)
const searchAlbum = useSelector((state) => state.search.albums)
const [releases, setReleases] = useState([])
useEffect(() => {
setReleases(newreleases)
}, [newreleases])
const history = useHistory()
const setRating = (ratingValue, index) => {
let updatedReleases = [...releases]
updatedReleases[index].rating = ratingValue // i'm assuming you have //rating field
setReleases(updatedReleases)
// then send this new releases to the api
}
const onAlbumCardClick = (dataId) => {
props.fetchAlbum(dataId)
history.push('/home/album')
}
return (
<section className='sm:flex sm:justify-between sm:flex-nowrap grid grid-cols-3'>
{view === 'noSearch' ? (
<>
{newreleases.slice(0, 5).map((data, index) => (
<AlbumCard
url={data.images[0].url}
key={data.id}
id={data.id}
albumname={data.name}
onClickStar={(ratingValue) => setRating(ratingValue, index)}
onClick={() => onAlbumCardClick(data.id)}
/>
))}
</>
) : view === 'search' ? (
<>
{searchAlbum.slice(0, 5).map((data, index) => (
<AlbumCard
url={data.images[0].url}
key={data.id}
id={data.id}
onClickStar={(ratingValue) => setRating(ratingValue, index)}
albumname={data.name}
onClick={() => onAlbumCardClick(data.id)}
/>
))}
</>
) : null}
</section>
)
}
const mapDispatch = { fetchAlbum, setView }
export default connect(null, mapDispatch)(AlbumList)
I implement your StarRating component in an optimized & efficient manner.
import React, { useState } from "react";
import { FaStar } from "react-icons/fa";
import { rateAlbum } from "../../store/actions/userAlbumRatingAction";
function StarRating({ width }) {
const [rating, setRating] = useState(0);
const ratePicture = (rating) => {
setRating(++rating);
console.log("clicked me");
rateAlbum({
rating: ++rating,
}); // Dispatch the action to save the rating
};
return (
<div className="flex h-full" style={{ width: width, display: "flex" }}>
{[...Array(5)].map((star, i) => (
<div key={i} className="flex items-center w-full">
<FaStar
className="md:m-1 w-full h-full delay-200 cursor-pointer"
color={i < rating ? "#ffc107" : "#e4e5e9"}
onClick={() => ratePicture(i)}
/>
</div>
))}
</div>
);
}
export default StarRating;
Let me know if you already implement rendering the StarRating component with its album's default rate to include that too in this code.
I hope this will solve all the problems related to rating.

Resources