DndKit transition delay on drop, useSortable - reactjs

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} />

Related

I can't get array.filter to work with useState

I am trying to delete an item from a shopping cart and I am using the filter hook to accomplish this. I have looked at the documentation for this and at the answers here on stack overflow. unfortunately no luck.
this is my code for the entire component. the function is of course "deleteItemFromBasket" and it is being called at the onclick on the delete button:
function CheckoutProduct({id, title, price, description, rating, category, image }) {
const [basket, addToBasket] = useAppContext();
const deleteItemFromBasket = (id) => {
addToBasket(basket.filter((task) => task.id !== id));
};
return (
<div>
{basket.map((element) => {
if (element === id) {
return (
<div className='grid grid-cols-5 border-b pb-4'>
{/* far left */}
<Image src={image} height={200} width={200} objectFit='contain' />
{/* middle */}
<div className="col-span-3 mx-5">
<p>{title}</p>
<p className='text-xs my-2 line-clamp-3'>{description}</p>
<button onClick={deleteItemFromBasket} className='button'>delete</button>
<h1>items ID in basket: {basket}</h1>
<h1>length of array: {basket.length}</h1>
</div>
{/* right */}
<div>
<p>${price}</p>
</div>
</div>
)
}
})}
</div>
)
}
here is the code of the context provider
import React, { createContext, useContext, useState } from 'react';
const AppContext = createContext();
export function AppWrapper({ children }) {
var [basket, addToBasket]= useState([]);
return (
<AppContext.Provider value={[basket, addToBasket]}>
{children}
</AppContext.Provider>
);
}
export function useAppContext() {
return useContext(AppContext);
}
looks like basket is an array of ids, not an array of objects. Assuming that is the case, then you need to change your delete function to
const deleteItemFromBasket = (id) => {
addToBasket(basket.filter((element) => element !== id));
};
This is assuming that addToBasket is actually just setting the basket, not just additive.
I eventually got it to work and it took a combination of the two answers so thank you everyone for the help!
notice below that the function "deleteItemFromBasket" is the same as joe's post but it to get it to work I needed to add the function in the onClick like zehan's answer. I don't know why this works so if anyone has an explanation as to why I'll save the answer spot for it! thanks friends
import React from 'react';
import Image from 'next/image'
import { useAppContext } from '../state'
function CheckoutProduct({id, title, price, description, rating, category, image }) {
const [basket, addToBasket] = useAppContext();
const deleteItemFromBasket = (item) => {
addToBasket((current) => current.filter((element) => element !== item));
};
return (
<div>
{basket.map((element) => {
if (element === id) {
return (
<div className='grid grid-cols-5 border-b pb-4'>
{/* far left */}
<Image src={image} height={200} width={200} objectFit='contain' />
{/* middle */}
<div className="col-span-3 mx-5">
<p>{title}</p>
<p className='text-xs my-2 line-clamp-3'>{description}</p>
{/* <button onClick={deleteItemFromBasket(element)} >delete item</button> */}
<button onClick={ ()=> deleteItemFromBasket(id)} className='button'>delete</button>
<h1>items ID in basket: {basket}</h1>
<h1>length of array: {basket.length}</h1>
</div>
{/* right */}
<div>
<p>${price}</p>
</div>
</div>
)
}
})}
</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.

Group Disclosures (Accordian) from Headless UI

I've just started using Headless UI. I'm trying to use the Disclosure component from Headless UI to render my job experiences.
Basically, I need "n" number of Disclosures which will be dynamically rendered and whenever one Disclosure is opened the others should close.
I am able to render the Disclosures dynamically, and they all have their individual states. (opening/closing a disclosure doesn't affect the other Disclosure).
All I want to do is to have only one disclosure open at a time. Opening another Disclosure should close all the remaining Disclosures.
I have gone through their docs but couldn't find a way to manage multiple Disclosure states together.
Here is my code:
import React, { useContext } from "react";
import { GlobalContext } from "../data/GlobalContext";
import { Tab, Disclosure } from "#headlessui/react";
import ReactMarkdown from "react-markdown";
const Experience = () => {
const { data } = useContext(GlobalContext);
const expData = data.pageContent.find(
(content) => content.__component === "page-content.experience-page-content"
);
return (
<div className="container h-screen">
<div className="flex h-full flex-col items-center justify-center">
<h3 className="">{expData.pageTitle}</h3>
<div className="flex min-h-[600px] flex-col">
{expData.jobs.map((job, i) => (
<Disclosure key={job.companyName} defaultOpen={i === 0}>
<Disclosure.Button
key={job.companyName + "_tab"}
className="px-4 py-3 dark:text-dark-primary"
>
{job.companyName}
</Disclosure.Button>
<Disclosure.Panel key={job.companyName + "_panel"}>
<p className="">
<span className="">{job.designation}</span>
<span className="">{" # "}</span>
<span className="">{job.companyName}</span>
</p>
<p className="">{job.range}</p>
<ReactMarkdown className="">
{job.workDescription}
</ReactMarkdown>
</Disclosure.Panel>
</Disclosure>
))}
</div>
</div>
</div>
);
};
export default Experience;
It would be really helpful if someone could help me with this.
Thanks.
Ok, I stole from various sources and managed to hack it. I haven't tested it for accessibility but it has some interesting things because it deviates a little bit (rather usefully if you ask me) from the React mental model.
The tldr is that you will need to trigger clicks on the other elements imperatively via ref.current?.click()
Here are the steps:
1) Create the refs:
Here we can't use hooks since you can't call hooks inside loops or conditionals, we use React.createRef<HTMLButtonElement>() instead
const refs = React.useMemo(() => {
return (
items.map(() => {
return React.createRef<HTMLButtonElement>();
}) ?? []
);
}, [items]);
2) Add the corresponding ref to the Disclosure.Button component
{items.map((item, idx) => (
<Disclosure key={item.id}>
{({open}) => (
<>
{/* other relevant stuff */}
<Disclosure.Button ref={refs[idx]}>
Button
</Disclosure.Button>
<Disclosure.Panel>
{/* more stuff */}
</Disclosure.Panel>
</>
)}
</Disclosure>)
)}
3) Use data attributes (for making it easy on yourself)
this one is gonna be specially useful for the next step
{items.map((item, idx) => (
<Disclosure key={item.id}>
{({open}) => (
<>
{/* other relevant stuff */}
<Disclosure.Button
ref={refs[idx]}
data-id={item.id}
data-open={open}
>
Button
</Disclosure.Button>
<Disclosure.Panel>
{/* more stuff */}
</Disclosure.Panel>
</>
)}
</Disclosure>)
)}
4) define your handleClosingOthers function (an onClick handler)
Basically here we get all the buttons that aren't the one that the user is clicking, verifying if they are open and if they are, clicking programmatically on them to close them.
function handleClosingOthers(id: string) {
const otherRefs = refs.filter((ref) => {
return ref.current?.getAttribute("data-id") !== id;
});
otherRefs.forEach((ref) => {
const isOpen = ref.current?.getAttribute("data-open") === "true";
if (isOpen) {
ref.current?.click();
}
});
}
5) finally we add that function to the onClick handler
{items.map((item, idx) => (
<Disclosure key={item.id}>
{({open}) => (
<>
{/* other relevant stuff */}
<Disclosure.Button
ref={refs[idx]}
data-id={item.id}
data-open={open}
onClick={() => handleClosingOthers(item.id)}
>
Button
</Disclosure.Button>
<Disclosure.Panel>
{/* more stuff */}
</Disclosure.Panel>
</>
)}
</Disclosure>)
)}
<template>
<div class="mx-auto w-full max-w-md space-y-3 rounded-2xl bg-white p-2">
<Disclosure v-for="(i, idx) in 3" :key="i" v-slot="{ open, close }">
<DisclosureButton
:ref="el => (disclosure[idx] = close)"
class="flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2 text-left text-sm font-medium text-purple-900 hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500 focus-visible:ring-opacity-75"
#click="hideOther(idx)"
>
<span> What is your refund policy? {{ open }} </span>
</DisclosureButton>
<DisclosurePanel class="px-4 pt-4 pb-2 text-sm text-gray-500">
If you're unhappy with your purchase for any reason, email us within 90 days and we'll
refund you in full, no questions asked.
</DisclosurePanel>
</Disclosure>
</div>
</template>
<script setup>
import { Disclosure, DisclosureButton, DisclosurePanel } from '#headlessui/vue'
const disclosure = ref([])
const hideOther = id => {
disclosure.value.filter((d, i) => i !== id).forEach(c => c())
}
</script>
here how I did it in Vue.
I've used this approach:
function Akkordion({ items }) {
const buttonRefs = useRef([]);
const openedRef = useRef(null);
const clickRecent = (index) => {
const clickedButton = buttonRefs.current[index];
if (clickedButton === openedRef.current) {
openedRef.current = null;
return;
}
if (Boolean(openedRef.current?.getAttribute("data-value"))) {
openedRef.current?.click();
}
openedRef.current = clickedButton;
};
return (
<div>
{items.map((item, idx) => (
<Disclosure key={item.id}>
{({ open }) => (
<div>
<Disclosure.Button as="div">
<button
data-value={open}
ref={(ref) => {
buttonRefs.current[idx] = ref;
}}
onClick={() => clickRecent(idx)}
>
{item.label}
</button>
</Disclosure.Button>
<Disclosure.Panel
>
{item.content}
</Disclosure.Panel>
</div>
)}
</Disclosure>
))}
</div>
);
}

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