React useState re rendering too many times - javascript

I am creating a simple carousel with react and I noticed that my index gets call multiple times and I don't understand why, here is my snippet of code here also here is a full version https://codesandbox.io/s/small-bash-4l7ix?file=/src/index.js
...
const pages = [
React.forwardRef((props, ref) => (
<animated.div ref={ref} style={{ ...props.style, background: 'lightpink' }}>
A
</animated.div>
)),
React.forwardRef((props, ref) => (
<animated.div ref={ref} style={{ ...props.style, background: 'lightblue' }}>
B
</animated.div>
)),
React.forwardRef((props, ref) => (
<animated.div ref={ref} style={{ ...props.style, background: 'lightgreen' }}>
C
</animated.div>
)),
]
export default function App() {
const [index, set] = useState(0)
const [containerRef, containerSize] = useDimensions()
const transitions = useTransition(index, p => p, {
from: { opacity: 0, transform: 'translate3d(100%,0,0)' },
enter: { opacity: 1, transform: 'translate3d(0%,0,0)' },
leave: { opacity: 0, transform: 'translate3d(-50%,0,0)' },
})
const divStyle = {
height: `${containerSize.height}px`,
}
console.log(index)
return (
<>
<button className={`btn ${index === 0 ? 'btn--active' : ''}`} onClick={() => set(0)}>
Slide 1
</button>
<button className={`btn ${index === 1 ? 'btn--active' : ''}`} onClick={() => set(1)}>
Slide 2
</button>
<button className={`btn ${index === 2 ? 'btn--active' : ''}`} onClick={() => set(2)}>
Slide 3
</button>
<div style={divStyle} className="simple-trans-main">
{transitions.map(({ item, props, key }) => {
const Page = pages[item]
return <Page ref={containerRef} key={key} style={props} />
})}
</div>
<p> Lorem ipusum</p>
</>
)
}
...

In order to transition components in and out the useTransition hook from react-spring keeps track of component instances for you. These extra renders are caused by overlapping mounting and unmounting of nodes.
For example:
Start at 'Slide 1'
Click 'Slide 2'
Library mounts 'Slide 2' // Triggers rerender
Library starts to transition 'Slide 1' out
Library unmounts 'Slide 1' // Triggers rerender
Each transition is pushed into the array and the library animates them in order. So you can trigger multiple overlapping animations simultaneously.
Checkout the React DevTools in the codesandbox and you will see the nodes mounting and unmounting.

This is because of useTransition.
That creates an extra render of sorts, that is why you see index printed multiple times.
I have removed the useTransition and you can check it prints only on index change.
Check below.

Related

How to map components and ensure the component has the same attribute individually

Currently I have a map function that render a serial of image, and I realized that they share the same hover state, which means they will perform the same action when hovered. Is there are any standard practice to map duplicate components while assigning them unique/individual properies?
{itemData.map((item) => (
<ImageListItem key={item.img}>
<img
src={item.img}
alt={item.title}
loading="lazy"
onMouseOver={() => {setHover(true)}}
onMouseOut={() => {setHover(false)}}
style={{ transform: hover ? 'scale(1.5, 1.5)' : null }}
/>
<ImageListItemBar
title={item.title}
subtitle={item.author}
actionIcon={
<IconButton
sx={{ color: 'rgba(255, 255, 255, 0.54)' }}
aria-label={`info about ${item.title}`}
>
<InfoIcon />
</IconButton>
}
/>
You should use a component, which create a unique state for each element, i wrote an easy to understand example.
import React, { useState } from "react"
const items = [
{
title: 'Card1',
price: 100
},
{
title: 'Card2',
price: 50
},
{
title: 'Card3',
price: 200
},
]
export default function App() {
return (
<>
{
items.map(element => {
return(
<Card {...element}/>
)
})
}
</>
)
}
function Card({title, price, key}) {
const [isHovered, setHover] = useState(false)
return (
<>
<div
key={key}
onMouseOver={() => {setHover(true)}}
onMouseOut={() => {setHover(false)}}
>
<div>
{title}
</div>
<h3>
{
isHovered && price
}
</h3>
</div>
</>
);
}
I made the card price to show if hovered so you can see it works on each individual component.
Code sandbox if you want to check it out.
To provide unique properties, you need to have something that uniquely identifies your image component and use it to manage your state. In your case, your state hover should be an array or an object, not a boolean. Since you are using item.img as a key, I assume it is unique and hence it can help in your state management like this:
const [hover, setHover] = useState({});
{itemData.map((item) => (
<ImageListItem key={item.img}>
<img
src={item.img}
alt={item.title}
loading="lazy"
onMouseOver={() => setHover({...hover, [item.img]: true})}
onMouseOut={() => setHover({...hover, [item.img]: false})}
style={{ transform: hover ? 'scale(1.5, 1.5)' : null }}
/>
<ImageListItemBar
title={item.title}
subtitle={item.author}
actionIcon={
<IconButton
sx={{ color: 'rgba(255, 255, 255, 0.54)' }}
aria-label={`info about ${item.title}`}
>
<InfoIcon />
</IconButton>
}
/>
))
}
If you want the state to be in the parent without going all the way to an array or object, you can use a number instead. If only one item at a time is going to be active, you can just use the index of the active item as the state:
const { useState } = React;
const things = ["foo", "bar", "baz"];
function Component() {
const [active, setActive] = useState(-1);
const updateActivity = (index) => setActive(index === active ? -1 : index);
return (
<ul>
{things.map((thing, index) => (
<li>
<button key={index} onClick={() => updateActivity(index)}>
{index === active
? <strong>{thing}</strong>
: thing}
</button>
</li>
))}
<li>Value: {active}</li>
</ul>
);
}
ReactDOM.render(
<Component />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Alternatively, in cases where you want multiple items to be simultaneously active, you can use a "bit flag" approach where each bit of the value represents whether or not the corresponding index is active:
const { useState } = React;
const things = ["foo", "bar", "baz"];
function Component() {
const [active, setActive] = useState(0);
const updateActivity = (index) => setActive(active ^ Math.pow(2, index));
return (
<ul>
{things.map((thing, index) => (
<li>
<button key={index} onClick={() => updateActivity(index)}>
{active & Math.pow(2, index)
? <strong>{thing}</strong>
: thing}
</button>
</li>
))}
<li>Value: {active} ({active.toString(2).padStart(3, "0")})</li>
</ul>
);
}
ReactDOM.render(
<Component />,
document.getElementById("react2")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="react2"></div>

Framer motion modal - Animate presence does not work

I am using modal with framer motion, the initial animation for the modal works fine, but on the exit it does not work at all. The modal disappears immediately for some reason.
This is where I am opening the modal:
const Agenda = () => {
const [modalOpen, setModalOpen] = useState(false);
const close = () => setModalOpen(false);
const open = () => setModalOpen(true);
return (
<>
<AnimatePresence
initial={false}
exitBeforeEnter={true}
>
{modalOpen && (
<Modal modalOpen={modalOpen} handleClose={close}>
<AgendaContent />
</Modal>
)}
</AnimatePresence>
</>
);
};
export default Agenda;
And here is the modal itself:
const newspaper = {
hidden: {
transform: "scale(0) rotate(720deg)",
opacity: 0,
transition: {
delay: 0.3,
},
},
visible: {
transform: " scale(1) rotate(0deg)",
opacity: 1,
transition: {
duration: 0.5,
},
},
exit: {
transform: "scale(0) rotate(-720deg)",
opacity: 0,
transition: {
duration: 0.3,
},
},
};
const Modal = ({ handleClose, children }) => {
return (
<Backdrop onClick={handleClose}>
<motion.div
drag
onClick={(e) => e.stopPropagation()}
className="modal"
variants={newspaper}
initial="hidden"
animate="visible"
exit="exit"
>
<img
className="close-icon"
src={CloseIcon}
alt="close"
onClick={handleClose}
/>
{children}
</motion.div>
</Backdrop>
);
};
export default Modal;
I followed this tutorial. I am not sure what I am doing wrong here, I am using the AnimatePresence there, any idea what could be wrong?
From the AnimatePresence docs:
Note: Direct children must each have a unique key prop so
AnimatePresence can track their presence in the tree.
It's frustratingly easy to forget even if you know about the requirement.
Something like this should work:
<AnimatePresence
initial={false}
exitBeforeEnter={true}
>
{modalOpen && (
<Modal key="modal" modalOpen={modalOpen} handleClose={close}>
<AgendaContent />
</Modal>
)}
</AnimatePresence>

How to target single item in list with onClick when mapping JSON array in React

I have a JSON file with an array of objects and each object consists of "Question" and "Answer" (I'm creating an FAQ section). What I'm doing is mapping over the array and displaying the list of questions, which works just fine. Next to each question is an icon and I want the icon to change when I click on it but it is changing EVERY icon in the list instead of just that one item that was clicked on.
I'm using Material UI and hooks and this is how I have my handleClick set up:
const [click, setClick] = useState(true);
const handleClick = () => {
setClick(!click);
};
This is how I have the array mapping set up:
<List
style={{
maxHeight: 430,
width: 500,
overflow: 'auto',
border: '1px solid black',
}}
>
{faqdata.map((item) => (
<ListItem style={{ cursor: 'pointer' }}>
<ListItemIcon>
{click ? <AddIcon /> : <RemoveIcon />}
</ListItemIcon>
<ListItemText primary={item.Question} onClick={handleClick} />
</ListItem>
))}
</List>
How can I make it to where the icon changes on only the list item that I click on instead of every list item in the list? Is my onClick in the incorrect spot? Any help would be greatly appreciated. Thanks!
Issue
You are using a single boolean value to store a "clicked" state, and all your mapped UI uses that single state to cue from.
Solution
Assuming you would like multiple items to be clicked, and also assuming your mapped data is static (i.e. the faqData isn't added to, removed from, or sorted) then using the mapped index to toggle the "clicked" state is acceptable. use an object to store "clicked" indices and update the handleClick callback to toggle the state. For this use case I like to make the callback a curried handler to enclose in scope the value I wish to use in the callback.
const [clickedIndex, setClickedIndex] = useState({});
const handleClick = (index) => () => {
setClickedIndex(state => ({
...state, // <-- copy previous state
[index]: !state[index] // <-- update value by index key
}));
};
...
<List
style={{
maxHeight: 430,
width: 500,
overflow: 'auto',
border: '1px solid black',
}}
>
{faqdata.map((item, index) => (
<ListItem style={{ cursor: 'pointer' }}>
<ListItemIcon>
{clickedIndex[index] ? <AddIcon /> : <RemoveIcon />} // <-- check if index is truthy in clickedIndex state
</ListItemIcon>
<ListItemText
primary={item.Question}
onClick={handleClick(index)} // <-- pass index to handler
/>
</ListItem>
))}
</List>
Is this what you're looking for?
import { useState } from "react";
import "./styles.css";
const faqdata = [
{ Question: "Q1", Answer: "A1" },
{ Question: "Q2", Answer: "A2" },
{ Question: "Q3", Answer: "A3" },
{ Question: "Q4", Answer: "A4" }
];
const AddIcon = () => <span class="icon">+</span>;
const RemoveIcon = () => <span class="icon">☓</span>;
function ListItem({ d }) {
const [checked, setChecked] = useState(false);
return (
<li
onClick={() => {
setChecked(!checked);
}}
>
{checked ? <RemoveIcon /> : <AddIcon />}
{d.Question}
</li>
);
}
function List() {
return (
<ul>
{faqdata.map((d) => {
return <ListItem d={d} />;
})}
</ul>
);
}
You can try it out here
The problem with the current approach is that there's only one variable to store the added/removed status of every question. So, when the click boolean updates, it updates the state of all elements.
In the code shared above, the ListItem component is responsible for maintaining the added/removed status of each question separately. So, one item in the list can change without affecting the other.
It's one of my test. You should save selected ids and check if the id exists in that array.
const [selectedfaqdataIds, setSelectedfaqdataIds] = useState([]);
const handleSelect = (event, id) => {
const selectedIndex = selectedfaqdataIds.indexOf(id);
let newselectedfaqdataIds = [];
if (selectedIndex === -1) {
newselectedfaqdataIds = newselectedfaqdataIds.concat(selectedfaqdataIds, id);
} else if (selectedIndex === 0) {
newselectedfaqdataIds = newselectedfaqdataIds.concat(selectedfaqdataIds.slice(1));
} else if (selectedIndex === selectedfaqdataIds.length - 1) {
newselectedfaqdataIds = newselectedfaqdataIds.concat(selectedfaqdataIds.slice(0, -1));
} else if (selectedIndex > 0) {
newselectedfaqdataIds = newselectedfaqdataIds.concat(
selectedfaqdataIds.slice(0, selectedIndex),
selectedfaqdataIds.slice(selectedIndex + 1)
);
}
setSelectedfaqdataIds(newselectedfaqdataIds);
};
{faqdatas.map((faqdata) => (
<ListItem style={{ cursor: 'pointer' }}>
<ListItemIcon>
{selectedfaqdataIds.indexOf(faqdata.id) !== -1}? <AddIcon /> : <RemoveIcon />}
</ListItemIcon>
<ListItemText primary={faqdata.Question} onClick={(event) => handleSelect(event, faqdata.id)} />
</ListItem>
))}

Is it possible to delay Fade's transition?

Goal: I want Fade's transition to happen after a certain time has passed (ex: 4000 milliseconds).
Here is a fragment of my code:
<Fade in timeout={{ enter: 8000 }}>
<Box display="flex" justifyContent="center">
<IconButton href="https://soundcloud.com/jyillamusic">
<Soundcloud />
</IconButton>
<IconButton href="https://www.instagram.com/justinyum98/">
<Instagram />
</IconButton>
</Box>
</Fade>
Expected result: With enter: 8000, I expect the transition to happen after 8000 milliseconds.
Actual result: The transition starts at 0 milliseconds, finishes after 8000 milliseconds.
Does Fade support delaying the Fade transition by some specified time?
(Looking at Fade's API, I assumed that duration.enteringScreen meant that it was the amount of milliseconds before the transition occurs, but I'm probably mistaken.)
In "Components > Transitions > Zoom demo" of Material UI doc there are some examples in order to achieve it; a bit more elegant than touch the state!
In esence, you can pass a style attribute to Fade component and set the delay (transitionDelay). In the code below we iterate a result array and causes each individual element be showed with a 100ms delay from previous.
{profileItems.map((pi, index) => (
<Fade in={profileItems.length > 0}
timeout={{ enter: 500, exit: 250 }}
style={{ transitionDelay: `${index * 100}ms` }}
key={`asi-${pi.key}-${index}`}>
<span>
<DisplayItem profileItem={pi} />
</span>
</Fade>))}
Fade does not implement a delay feature, however you can manually handle your transition status using in.
in
Show the component; triggers the enter or exit states
type: boolean
default: false
In code you could do :
<Fade in={this.state.in} timeout={{ enter: 8000 }}>
<Box display="flex" justifyContent="center">
<IconButton href="https://soundcloud.com/jyillamusic">
<Soundcloud />
</IconButton>
<IconButton href="https://www.instagram.com/justinyum98/">
<Instagram />
</IconButton>
</Box>
</Fade>
And on display run a timeout to wait and create the delay
this.state = {
in: false
};
setTimeout(() => {
setState({ in: true });
}, 8000);
To piggyback off of sebastienbarbier, you could create a custom transition component that will handle the state for you by simply passing a delay prop.
Here is what the component would look like (I added additional useful props):
const MyFade = ({
children,
in: In = true,
timeout = 1000,
delay = 0
}) => {
const [isIn, setIsIn] = useState(In && delay === 0)
useEffect(() => {
if (delay > 0) {
setTimeout(() => setIsIn(true), delay)
}
})
return (
<Fade in={isIn} timeout={timeout}>
{children}
</Fade>
)
}
And then for your specific issue to have it wait 4000ms:
<MyFade in timeout={{ enter: 8000 }} delay={4000}>
<Box display="flex" justifyContent="center">
<IconButton href="https://soundcloud.com/jyillamusic">
<Soundcloud />
</IconButton>
<IconButton href="https://www.instagram.com/justinyum98/">
<Instagram />
</IconButton>
</Box>
</MyFade>
This seem to work for me without Fade:
const [open, setOpen] = React.useState(false);
<List>
{[
'Inbox',
'Starred',
'Send email',
'Drafts',
'foo',
'bar',
'baz',
].map((text, index) => (
<ListItem
key={text}
sx={{
transitionProperty: 'all',
transitionDuration: '750ms',
...(open && {
opacity: 1,
transform: 'translateY(0)',
transitionDelay: `${index * 150}ms`,
}),
...(!open && {
opacity: 0,
transform: 'translateY(100%)',
}),
}}
>
or
<ListItem
key={text}
style={{ transitionDelay: open ? `${index * 150}ms` : '0ms' }}
>

multiple iteration is done for creating the generic filters

I am trying to develop a generic filter component which can have many fields to filter on like color,
size, price range etc and each field might have different types of elements like color may have
checkboxes, radio button and price range might have input element, dropdown etc. To support such varied
cases, I tried to go with this pattern but here I have to iterate the same things multiple times.
I am not sure of this data structure. If anyone has suggestion please help me to improve this code but
the main problem here is "multiple iteration". How can i improve this code?
const filterParams = {
field: {
id : 1, label : 'Field', content: <FieldFilter />
},
employee: {
id : 1, label : 'Employee', content: <Employee />
}
}
<Filter filterParams={filterParams} activeFilterParam="field" />
const Filter = ({ filterParams, activeFilterParam }) => {
const [show, setShow]=useState(false)
return (
<>
<Button secondary icon={filter} onClick={() => setShow(!show)}>Filter</Button>
{show && (
<Card style={{ marginTop: 10 }}>
<Card.Content>
<Tabs activeTab={activeFilterParam}>
<Tabs.List
render={() => {
return (
Object.keys(filterParams).map(filterParam => {
return (
<Tabs.Tab key={filterParam} id={filterParam}>{filterParams[filterParam].label}</Tabs.Tab>
)
}))
}} />
<Tabs.Panels>
{Object.keys(filterParams).map(filterParam => {
return (
<Tabs.Panel key={filterParam} panelId={filterParam}>{filterParams[filterParam].content}</Tabs.Panel>
)
})}
</Tabs.Panels>
</Tabs>
</Card.Content>
<Card.Footer>
<Button>
<Button.Content style={{ marginRight: 10 }}>Save</Button.Content>
<Button.Content secondary onClick={()=>setShow(!show)}>Cancel</Button.Content>
</Button>
</Card.Footer>
</Card>
)}
</>
)
}
If you're not liking the multiple calls to Object.keys(filterParams).map, you could move the loop to the top of the component function. Something like the below might work:
const Filter = ({ filterParams, activeFilterParam }) => {
const [show, setShow]=useState(false)
const {tabs, panels} = Object.keys(filterParams)
.reduce((acc, filterParam) => {
acc.tabs.push(
<Tabs.Tab key={filterParam} id={filterParam}>{filterParams[filterParam].label}</Tabs.Tab>
);
acc.panels.push(
<Tabs.Panel key={filterParam} panelId={filterParam}>{filterParams[filterParam].content}</Tabs.Panel>
);
return acc;
}, { tabs: [], panels: [] });
return (
...
<Card style={{ marginTop: 10 }}>
<Card.Content>
<Tabs activeTab={activeFilterParam}>
<Tabs.List render={() => tabs} />
<Tabs.Panels>
{panels}
</Tabs.Panels>
</Tabs>
...
</Card>
...
)
}
Note that I haven't run this - it likely won't be quite right, but should give the general idea...

Categories

Resources