React is complaining that I'm causing too many re-renders - reactjs

The renderDialogue function should render Dialogue when the window size is less than 654px, otherwise it should update the state of expandMenu to false. However, React is saying there are too many re-renders. How to solve this?
const [expandMenu, setExpandMenu] = useState(false);
const handleOpenMenu = () => {
setExpandMenu(!expandMenu);
};
const renderDialogue = () => {
if (window.innerWidth < 654) {
return (
<Dialog
open={expandMenu}
handler={() => handleOpenMenu()}
size={"xl"}
className={"bg-transparent shadow-none "}
>
<DialogBody>
<div className={"relative"}>
<img
src={"/imgFolder2/cloud.webp"}
className={"h-full float-center"}
/>
<ul className="flex flex-col justify-center h-[75%] gap-5 text-2xl text-center absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] w-[55%]">
{NavItems()}
</ul>
</div>
</DialogBody>
</Dialog>
);
} else {
setExpandMenu(false);
return <> </>;
}
};
Here is the NavItems function, which iterates through an array of links.
const NavItems = () => {
return paths.map((path, idx) => (
<li key={idx}>
<Link
href={getPath(idx)}
className={`text-black block ${
isSelectedPath(getName(idx, true))
? "border-b-4 border-buttonBG"
: ""
}`}
onClick={() => {
if (window.innerWidth < 654) setExpandMenu(!expandMenu);
}}
>
{getName(idx)}
</Link>
</li>
));
};

First of all, your component will never re-render when the window size changes. This means that your if-statement where you display something depending on the window width will only fire on first render. Which probably is fine, but not recommended.
Secondly, your error "too many re-renders" is because you invoke setExpandMenu directly in your component. Each time you update your expandMenu state, this component will re-render. Then you update it again, so it will re-render again. See the infinite loop here?
Below is a working example of what you want, including updating on window resize. I've added comments to explain what is happening:
const [expandMenu, setExpandMenu] = useState(false);
const [windowWidth, setWindowWidth] = useState(0)
useEffect(() => { // This will update your state depending on your window size
function updateSize() {
setWindowWidth(window.innerWidth)
}
window.addEventListener('resize', updateSize);
updateSize();
return () => window.removeEventListener('resize', updateSize);
}, []);
const handleOpenMenu = () => {
setExpandMenu(!expandMenu);
};
const renderDialogue = () => {
if (windowWidth > 654 || expandMenu) {
return (
<Dialog
open={expandMenu}
handler={() => handleOpenMenu()}
size={"xl"}
className={"bg-transparent shadow-none "}
>
<DialogBody>
<div className={"relative"}>
<img
src={"/imgFolder2/cloud.webp"}
className={"h-full float-center"}
/>
<ul className="flex flex-col justify-center h-[75%] gap-5 text-2xl text-center absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] w-[55%]">
{NavItems()}
</ul>
</div>
</DialogBody>
</Dialog>
);
} else {
// setExpandMenu(false); <--- This line is causing rerenders: Remove it!
return <> </>;
}

Related

Increment counter and make other counters 0

I have created a counter component and using it in parent component four times.
Below is the code from Counter component
function NoTravellers({ label, chooseTraveller }) {
const [count, setCount] = useState(0);
const reduceCount = () => {
if (count === 0) {
return;
} else {
setCount((count) => count - 1);
}
};
const increaseCount = () => {
if (count >= 2) {
return;
} else {
setCount((count) => count + 1);
}
};
useEffect(() => {
if (count > 0) {
chooseTraveller(count);
}
}, [count]);
return (
<div className="flex justify-around mb-8 w-1/3">
<div>
<span className="font-semibold text-sm">{label}</span>
</div>
<div className="flex space-x-4 justify-center">
<MinusCircleIcon
className="w-6 h-6 text-[#33C5B4]"
onClick={reduceCount}
/>
<span className="text-base text-[#565656] font-semibold">{count}</span>
<PlusCircleIcon
className="w-6 h-6 text-[#33C5B4]"
onClick={increaseCount}
/>
</div>
</div>
);
}
export default NoTravellers;
And I'm using it in parent component like below -
<div className="flex flex-col justify-center">
<div className="flex justify-around">
<NoTravellers label="Adults" chooseTraveller={(traveller) => chooseTravellers(traveller, 1)} />
<NoTravellers label="Students" chooseTraveller={(traveller) => chooseTravellers(traveller, 2)} />
</div>
<div className="flex justify-around">
<NoTravellers label="Seniors" chooseTraveller={(traveller) => chooseTravellers(traveller, 3)} />
<NoTravellers label="Childrens" chooseTraveller={(traveller) => chooseTravellers(traveller, 4)} />
</div>
</div>
What I needed if users increase the value of one counter, the other three counters value will reset to 0.
How can I achieve this?
Thanks in advance....
The simplest solution is pulling the counts up into the parent that way you can easily modify them in one place. Then just pass the count down into each child.
If you store the state in the children modifying it together is difficult.
const Parent = () => {
const [counts, setCounts] = useState([0,0,0]);
const updateCount = (idx, count) => {
const newCounts = [0,0,0]
newCounts[idx] = count;
setCounts(newCounts);
};
return <>
{counts.map((count,idx) => <Child count={count} update={(count) => updateCount(idx, newCount)}/>}
</>
};
const Child = ({count, update}) => {
return <button onClick={() => update(count+1)}>{count}</button>
}
^ this is not tested code I didn't setup a real react project but you should be able to use the same concept in your setup.

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

DnD-Kit Incorrect behavior while trying to move item to another container in ReactJs

I would appreciate any help with this case, so if you see any minor issue - please, write me. There will be rather a lot of code.
I was trying to implement 'dnd-kit/sortable' into my bug tracker app. I have Kanban board consisting of four repeating column components. I needed to implement dnd-kit to be able to move task cards not only inside of each column, but between columns as well. Current code with sorting task cards in column, but if you try to move a card to any other column - most of the time nothing happens, but sometimes you get the Uncaught TypeError: Cannot read properties of undefined (reading 'id') I red through documentation many times and looked through similar projects in open source, but couldn't find what could be the reason for this bug.
The tasks from TasksContext is object with keys backlog, todo, inProgress, inReview, done and contains array of object. Each object inside of array represents task card.
Dashboard.js
const Dashboard = () => {
const { tasks, setTasks } = useContext(TasksContext)
const [activeId, setActiveId] = useState(null);
const mouseSensor = useSensor(MouseSensor);
const touchSensor = useSensor(TouchSensor);
const sensors = useSensors(mouseSensor, touchSensor)
const fullArray = Array.from(Object.values(tasks).flat())
console.log(fullArray)
const handleDragStart = ({ active }) => setActiveId(active.id);
const handleDragCancel = () => setActiveId(null);
const handleDragEnd = ({active, over}) => {
const { containerId: activeContainer } = active.data.current.sortable
const { containerId: overContainer } = over.data.current.sortable
const oldIndex = tasks[activeContainer].findIndex(obj => obj.id === active.id);
const newIndex = tasks[overContainer].findIndex(obj => obj.id === over.id);
if (active.id !== over.id) {
setTasks((prevTasks) => ({
...prevTasks,
[overContainer]: arrayMove(prevTasks[overContainer], oldIndex, newIndex)
}));
}
setActiveId(null);
}
return (
<div className='relative grid grid-cols-4 gap-6 px-6 grow-0 shrink-0 basis-5/6 overflow-y-scroll'>
<DndContext sensors={sensors} collisionDetection={rectIntersection} onDragStart={handleDragStart} onDragCancel={handleDragCancel} onDragEnd={handleDragEnd}>
<TasksColumn key='to do' title='to do' id='todo' tasks={tasks.todo} />
<TasksColumn key='in progress' title='in progress' id='inProgress' tasks={tasks.inProgress} />
<TasksColumn key='in review' title='in review' id='inReview' tasks={tasks.inReview} />
<TasksColumn key='done' title='done' id='done' tasks={tasks.done} />
<DragOverlay>{activeId ? <TaskCard id={activeId} task={fullArray.filter(task => task?.id === activeId)[0]} /> : null}</DragOverlay>
</DndContext>
</div>
)
}
TasksColumn.js
const TasksColumn = ({ title, id, tasks }) => {
const { setNodeRef } = useDroppable({id});
return (
<div className=''>
<ColumnHeader title={title} id={id} />
<div className="h-3 w-full border-b-2 border-grayDark" />
<SortableContext items={tasks} id={id} strategy={verticalListSortingStrategy}>
<div ref={setNodeRef} className=''>
{tasks.map(task => (
<Draggable key={task.name} id={task.id} task={task} />
))}
</div>
</SortableContext>
</div>
)
}
Draggable.js
const Draggable = ({ id, task }) => {
const { setNodeRef, transform, transition, isDragging, } = useSortable({id});
const style = {
transform: CSS.Translate.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
return (
<div ref={setNodeRef} style={style}>
<TaskCard id={id} task={task} />
</div>
)
}
TaskCard.js
const TaskCard = ({ id, task }) => {
const { attributes, listeners, setActivatorNodeRef } = useSortable({id});
return (
<div className="py-4 border-b-2 border-grayLight">
<div className="">
<p className="">{task.deadline}</p>
<p className="">{task.priority}</p>
</div>
<ArrowsPointingOutIcon className='rotate-45 w-5 h-5 outline-none' ref={setActivatorNodeRef} {...listeners} {...attributes} />
<p className="">{task.name}</p>
<div className="">
<p className="">{task.author}</p>
<p className="">{task.time}</p>
</div>
</div>
)
}

How to set hover in a React loop and effect only one instead of all elements in the loop?

When I use setHover it reflects to all list data which returned from map loop. How can I use hover to reflect on itself element?
const [hover, setHover] = useState(true)
function MouseOver(event) {
setHover(true)
}
function MouseOut(event){
setHover(false)
}
{data.map((item, index) => (
//When I hover parent div I want to show the {item.arrow} div inside and not all {item.arrow} divs in the loop
<div key={index} onMouseEnter={MouseOver} onMouseLeave={MouseOut} className="flex gap-3">
<div>
{item.content}
</div>
<div hidden={hover}>
{item.arrow}
</div>
</div>
))}
If the state does not need to be controlled by the parent you can create a new component to use in the list.
Each component will then control its own hover state.
const List = ({data}) => {
return (
<div>
{
data.map((item, index) => (<Item key={index} item={item} />))
}
</div>
)
}
const Item = ({item}) => {
const [hover, setHover] = useState(true)
const mouseOver = (event) => {
setHover(true)
}
const mouseOut = (event) => {
setHover(false)
}
return (
<div onMouseEnter={mouseOver} onMouseLeave={mouseOut} className="flex gap-3">
<div>
{item.content}
</div>
<div hidden={hover}>
{item.arrow}
</div>
</div>
);
}
If the state does need to be controlled by the parent you can use a Record<number, boolean> to store the states.
const List = ({data}) => {
const [hover, setHover] = useState({})
const mouseOver = (event, index) => {
setHover(c => {
return {
...c,
[index]: true
};
})
}
const mouseOut = (event, index) => {
setHover(c => {
return {
...c,
[index]: false
};
})
}
return (
<div>
{
data.map((item, index) => (
<div
key={index}
onMouseEnter={(e) => {
mouseOver(e, index);
}}
onMouseLeave={(e) => {
mouseOut(e, index);
}}
className="flex gap-3"
>
<div>
{item.content}
</div>
<div hidden={hover[index]}>
{item.arrow}
</div>
</div>
))
}
</div>
)
}
If the state is not needed for anything other than hiding a div you could also just use CSS.
CSS will not require the component to rerender everytime you hover over it.
CSS
.hoverable-show {
display: none;
}
.hoverable-item:hover .hoverable-show {
display: block;
}
JS
const List = ({data}) => {
return (
<div>
{
data.map((item, index) => (
<div
className="flex gap-3 hoverable-item"
>
<div>
{item.content}
</div>
<div className="hoverable-show">
{item.arrow}
</div>
</div>
))
}
</div>
)
}
Preference should be CSS -> Individual State -> Parent (list) State.
This looks like a use case for the useReducer hook available right from the react library.

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

Resources