Nextjs SSG Suspense throws Hydration Error - reactjs

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.

Related

DndKit transition delay on drop, useSortable

When I drop an element to resort, there's a slight transition issue where the items goes back to its' original position & then transitions. Below I added a link to video & code.
Really, I don't want any transition delay, etc. I feel like it could be the remapping of the array, but i've tested it by optimizing as much as possible & still getting the same issue. It works fine without the transition property on there, but would like it to make things feel smooth.
Link to video --> Link to video
import { useSortable } from '#dnd-kit/sortable';
import { CSS } from '#dnd-kit/utilities';
import React from 'react';
import Icon from '../../../../../../../common/components/icon/Icon';
interface Props {
id: number;
children: React.ReactNode;
}
const SortableItem = ({ id, children }: Props) => {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: id });
return (
<div
ref={setNodeRef}
style={{
transform: CSS.Transform.toString(transform),
transition,
}}>
<div className='grid grid-cols-[60px_auto] items-center w-full h-full relative'>
{/* Make this the icon grabble */}
<div
className='flex items-center w-full h-full cursor-grab'
data-grab={true}
{...attributes}
{...listeners}>
<Icon name='ArrowsUpDownLeftRight' width={20} height={20} />
</div>
{children}
</div>
</div>
);
};
export default SortableItem;
'use client';
import React, { useState } from 'react';
import { ListenService } from '../SmartLinkClient';
import Icon from '../../../../../../common/components/icon/Icon';
import Input from '../../../../../../common/components/input/Input';
import ToggleSwitch from '../../../../../../common/components/toggle-switch/ToggleSwitch';
import Hr from '../../../../../../common/components/hr/Hr';
import { DndContext, PointerSensor, useSensor, useSensors, closestCenter } from '#dnd-kit/core';
import { SortableContext, arrayMove, verticalListSortingStrategy } from '#dnd-kit/sortable';
import SortableItem from './(edit-services-partials)/SortableItem';
interface Props {
servicesConfig: ListenService[];
handleServiceShowToggle: (e: any, elementToChange: ListenService) => void;
handleServiceDragEnd: (active: any, over: any) => void;
}
const EditServices = ({ servicesConfig, handleServiceShowToggle, handleServiceDragEnd }: Props) => {
const sensors = useSensors(useSensor(PointerSensor));
return (
<div className='select-services'>
<div className='flex flex-col gap-y-4'>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={({ active, over }) => {
handleServiceDragEnd(active, over);
}}>
<SortableContext
items={servicesConfig.map((service: ListenService) => service.id)}
strategy={verticalListSortingStrategy}>
{/* Components that use the useSortable hook */}
{servicesConfig.map((service: ListenService, i: number) => (
<SortableItem key={i} id={service.id}>
<div className='grid grid-cols-[minmax(160px,180px)_minmax(200px,auto)_100px] items-center'>
<div className='grid grid-cols-[24px_auto] items-center gap-x-3 dark:text-stone-100 text-stone-800'>
<Icon name={service.iconName} width={24} height={24} color={service.color} />
<span className='text-[16px]'>{service.name}</span>
</div>
<Input
placeholder='We could not find a valid URL, but you can enter your own.'
type={'url'}
/>
<div className='justify-self-end -mt-2 flex flex-row items-center gap-x-2'>
<span className='text-[11px] text-stone-600 dark:text-stone-300'>Show</span>
<ToggleSwitch
toggled={service.show}
handleToggled={(e: any) => {
handleServiceShowToggle(e, service);
}}
/>
</div>
</div>
{i !== servicesConfig.length - 1 && (
<span className='col-span-2'>
<Hr />
</span>
)}
</SortableItem>
))}
</SortableContext>
</DndContext>
</div>
</div>
);
};
export default EditServices;
I ended up figuring it out. The key on the <SortableItem /> component must be the same as the id on the <SortableItem /> component. Weird, but makes sense.
Previous:
ex) <SortableItem key={i} id={service.id} />
Current (Solution):
ex) <SortableItem key={service.id} id={service.id} />

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>

I have an error trying to fetch data from database to frontend using GET method

Compiled with problems:X
ERROR
src\component\Products.jsx
Line 8:34: React Hook "useState" is called in function "getAllProducts" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use" react-hooks/rules-of-hooks
I'm trying to use useState, what is the best way to fetch the data from database to frontend
import React, { useState } from 'react';
import { NavLink } from 'react-router-dom';
import DATA from '../Data';
const getAllProducts = () => {
const [products, getproducts] = useState({
title : '',
price : '',
image : ''
});
const {title, price, image}= products;
let getproduct = fetch('http://localhost:6000/products/allProducts', {
method : 'GET',
headers : {
'Content-Type':'application/json'
},
body : JSON.stringify({
title, price, image
})
})
const cardItem = (item) => {
return(
<div className="card mx-3 my-5 py-3 px-2" key={item.id} style={{width: "15rem"}} id="cards">
<img src={item.image} className="card-img-top" alt={item.title} />
<div className="card-body text-center">
<h5 className="card-title fw-bolder" id="para">{item.title}</h5>
<p className="lead fw-bold" id="para">${item.price}</p>
<NavLink to={`products/${item.id}`} className="btn btn-outline-danger fw-bolder px-5 rounded-pill" id="para">Buy Now</NavLink>
</div>
</div>
);
}
return (
<div>
<div className="container py-2 mt-5 pt-5">
<div className="row">
<div className="col-12 text-center">
<h1 className="display-6 fw-bolder text-center" id="late">Latest Collections</h1>
<hr/>
</div>
</div>
</div>
<div className="container" id="products">
<div className="row justify-content-around">
{getproduct.map(cardItem)}
</div>
</div>
</div>
);
}
export default getAllProducts;
Hi
First I notify you of errors in your code and then expose you a solution:
In getAllProducts you use a useState but you can't use it this way, it's not a React component.
You also call getAllProducts.map but getAllProducts doesn't return any data array
In your useState the initial value represents ONLY one object of your data array
I would advise you this one , it's near of what you have done.
We create a method to get the data, a state to store the data and we display them conditionally
In my example below I use the axios library, it is an alternative to the fetch method that you use but the principle remains the same with the fetch method (except for one line of code)
I hope it helps you
import React , {useState,useEffect} from 'react'
import axios from 'axios'
const getAllProducts = async ()=>{
//Here is the fetch method
try{
const fetchQuery = await axios.get('my-url')
return fetchQuery.data
}
catch(error){
console.log(error)
}}
const CardItem = (props)=>(
//here you card component that will be mapped
<div>
<p>{props.data.title}</p>
<p>{props.data.price}</p>
</div>
)
const MappedCards = ()=>{
//Our initial state is a empty array
const [allProducts,setProducts] = useState([])
useEffect(()=>{
// I use the hook useEffect so when the MappedCards component is mounted
// I can fetch data and use the setProducts method
const initProducts = async ()=>{
const data = await getAllProducts()
setProducts(data)
}
//I call the method to initProducts
initProducts()
},[])
// I can even make something really cool here
if(allProducts.length === 0){
return <span>Loading data...</span>
}
// Now when allProducts will be filled with data I'll show this
return (<div className="my-container">
<h1>My shop</h1>
{allProducts.map(product => (<CardItem key={product.id} data={product} />}
</div>)
}
export MappedCards

React - Prevent re-render whole list when delete element

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);

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