React - Framer Motion animates only the last item in map - reactjs

so I have a state with an array, and I'm mapping it with framer motion animation. The problem is that I'm adding an item to the first position on the array, not at the end. So when a new item is added, instead of animating the new item added to the array, framer motion animates always the last one. Here is my code:
const [list, setList] = useState([])
const addToList = () => {
let newElement = (Math.random() + 1).toString(36).substring(7)
setList(prevState => ([newElement, ...prevState]))
}
return (
<div className="flex flex-col bg-white h-screen">
<button onClick={() => addToList()} className="bg-blue-500 p-2 rounded-lg text-white">Add to List</button>
<p className="mt-2">List:</p>
<div className="flex flex-col mt-4">
{
list.map((l, i) => {
return (
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }}>
<p key={i}>{l}</p>
</motion.div>
)
})
}
</div>
</div>
)
Always the same last item animates when a new item is added (instead of the new item that will be the first position at the array). How can I solve it?

Problem solved. I was using a key based on the array position of the element, so when a new element was added, React have to render everything again (I think). By using a unique key, React knows exactly what item was changed (added in this case). So the animation works fine.

Related

Why is useState and DOM not recreating/identifying/re-rendering the new state even with useState([...Array])?

I am trying to render and then re-render using array.map() an array of objects. The useEffect is called whenever the state(selected) changes for the options ('All','BACKEND','FRONTEND','ML').
The
All Selected rendering all data
Backend Selected showing "backendData"
Frontend Selected etc etc
MY ISSUE:
The rendering of the data is fine but I have added a basic animation which fades the elements in one by one using "react-awesome-reveal" library. I did the same with plain CSS so I don't think my error lies here.
The animation works if you refresh or direct towards the page or THE NUMBER OF ELEMENTS TO BE RE-RENDERED IS MORE THAN THE ELELEMNTS ON SCREEN THE LATER ELEMENTS WILL HAVE THE ANIMATION.
EG: If I go from "Frontend" to "Backend" then there are currently 2 elements in the DOM but now 3 need to be re-rendered so the first 2 elements do not carry the animation but the third does. It also works in the reverse where if I go from "All" 9 elements to "ML" 2 elements the elements are automatically rendered without the animation.
It seems like the DOM is looking at what is already rendered and deciding to not re-render the elements that are already there thus the FADE animation is not working.
Ideally I want the setData(...) to create a new state so that the array is mapped as if it the page was refreshed.
Here is the code snippet:
import {
backendCourses,
frontendCourses,
mlCourses,
allCourses,
} from "../../data";
export default function Courses() {
const [selected, setSelected] = useState();
const [data, setData] = useState(allCourses);
const list = [
{ id: "all", title: "All" },
{ ....},
...
];
useEffect(() => {
switch (selected) {
case "all":
setData(Array.from(allCourses));
break;
case "backend":
setData([...backendCourses]);
case ....
}, [selected]);
return (
<div className="courses" id="courses">
<h1>Courses</h1>
<ul>
{list.map((item, index) => (
<CoursesList
...
active={selected === item.id}
setSelected={setSelected}
/>
))}
</ul>
<div className="container">
{data.map((item, index) => (
<Fade ...>
<div ...>
<img ...>
</div>
</Fade>
))}
</div>
</div>
);
}
//CoursesList Component
export default function CoursesList({ id, title, active, setSelected }) {
return (
<li
className={active ? "coursesList active" : "coursesList"}
onClick={() => setSelected(id)}
>
{title}
</li>
);
}
The mapping of the elements using Fade from "react-awesome-reveal" FULL-CODE.
<div className="container">
{data.map((item, index) => (
<Fade delay={index * 500}>
<div className="item">
<img src={item.img} alt={item.title} id={index}></img>
<a href={item.url} target="_blank">
<h3>{item.title}</h3>
</a>
</div>
</Fade>
))}
Here is the full useEffect hook code. I initially thought I was not creating a new array only referencing it but have tried multiple ways to clone the array without success. The courses data for now is hard-coded and just read from a file.
useEffect(() => {
switch (selected) {
case "all":
setData(Array.from(allCourses));
break;
case "backend":
setData([...backendCourses]);
break;
case "frontend":
setData([...frontendCourses]);
break;
case "ml":
setData([...mlCourses]);
break;
default:
setData([...allCourses]);
}
}, [selected]);
Sorry for the long post but trying to be as detailed as I can.
It seems like the DOM is looking at what is already rendered and deciding to not re-render the elements that are already there thus the FADE animation is not working.
It's React that does that, not the DOM. That's part of its job: To avoid recreating DOM elements that already have the content you're trying to show.
If you want React to think you're providing new elements every time, even when in fact they're the same, give the elements a new key. At the moment, your map call isn't giving them a key at all (which it should be), so React defaults to using the element's position in the list as a key.
Instead, provide a key on the Fade component and make sure it changes when selected changes, probably by using selected itself in the key:
return (
<div className="container">
{data.map((item, index) => (
// I'm assuming `item.url` is unique within the array, adjust this
// if not. (*Don't* use `index`, indexes make poor keys, see the
// link above.
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
<Fade key={selected + "::" + item.url} delay={index * 500}>
<div className="item">
<img src={item.img} alt={item.title} id={index}></img>
<a href={item.url} target="_blank">
<h3>{item.title}</h3>
</a>
</div>
</Fade>
))}
</div>
);
That way, the elements get reused correctly when the component is rendered for other reasons (because each of them has a stable key), but when you change selected, they all get replaced with new elements because the key changes.

How to force update current slide in Swiper?

I have some images and slider with these images. I'm trying update current active slide with onClick event: (clicked image must be active slide)
In following code i've tried to change first-indexed image, but it doesn't work with Swiper, so I need force rerender my Swiper-slider, for the reason it started from first.
So the main goal is to make the clicked image the current slide
const [prev, setPrev] = useState()
const [ImageArray, setImageArray] = useState([item1, item2, item3])
const [slidesArray, setSlidesArray] = useState([item1, item2, item3])
const swiper = useSwiper()
const clickHandler = (image: any) => {
if (prev !== image) { //if clicked not the same
let index = slidesArray.indexOf(image) // find index of image in image array
let temp = [...slidesArray]
temp.splice(index, 1) //delete selected image
temp.unshift(image) // set first
setSlidesArray([...temp])
setPrev(image)
swiper.reInit() //not working
}
}
<div className={classes['Gallery__container']}>
{ImageArray.map((image, index) => (
<img src={image} alt="" key={index} width={200} height={200} onClick={() => clickHandler(image)}/>
))}
</div>
<Swiper
spaceBetween={50}
slidesPerView={'auto'}
loop
zoom={true}
>
{slidesArray.map((image,index) => (
<SwiperSlide key={index}>
<img src={image} alt="" width={200} height={200}/>
</SwiperSlide>
))}
</Swiper>
P.S. my code is working and first slide is changing, but I need to change the current

Detect only first change/render of a ref in react and scroll to bottom

DESIRED RESULT: I am trying to create a chat panel that has a scrollable container which contains all the messages. The container has an initial loading state, and when the data for that container is fetched then the container is rendered. What I am trying to achieve is that, when the container first renders after the loading is complete it will scroll to the bottom of the container. After that any change on that container won't interfere with the scroll. Only when a new message is sent, a socket event will be fired and it will trigger the container again to scroll to bottom.
PROBLEM: When giving react-query's isFethcedAfterMount and a socket event messageCreated as the dependency of useEffect() it works, but isFethcedAfterMount always changes whenever incoming or outgoing message is sent and the API is refetched and the container scrolls to bottom. This will hamper user experience as whenever he/she is scrolling through old messages any new message coming will scroll the container to bottom.
So What I want is that the chat container scrolls to bottom when the container first renders or the ref of the container only changes for the first time and after this change is detected then only messageCreated event will affect the scrollToBottom function.
Here's the code that I am using to implement all of the logic. Also tried to comment each section for better understanding. Please let me know if I missed something.
// check ref and scroll to the ref's bottom
const scrollToBottom = (ref) => {
if (ref) {
ref.current?.addEventListener("DOMNodeInserted", (event) => {
const { currentTarget: target } = event;
target.scroll({ top: target.scrollHeight });
});
}
};
// main component
const MessageList = () => {
const router = useRouter();
const conversationId = +router.query.conversationId;
// react query to get data from API and a loading state
const { isLoading, data: messageData, isFetchedAfterMount } = useQuery(["conversation-messages", conversationId], () => getMessages(conversationId), {
refetchOnWindowFocus: false,
enabled: !!conversationId,
staleTime: Infinity,
});
// declaring ref of the message list container
const messagesEndRef = useRef(null);
// get the event when a message is sent
const messageCreated = useConversationStore((state) => state.messageCreated);
useEffect(() => {
if (isFetchedAfterMount) scrollToBottom(messagesEndRef);
}, [isFetchedAfterMount, messageCreated]);
// messageCreated dependency triggers scrollToBottom each time it changes
// isFetchedAfterMount dependency triggers the useEffect hook to render on first mount
// but the problem of isFetchedAfterMount is that it changes each time new data is fetched from API
return isLoading ? (
<div className="w-full h-full flex justify-center items-center">
<ClipLoader color="#38bdf8" />
</div>
) : (
// after loading finishes get the ref of this message list container and scroll to it's bottom
<ul className="flex flex-col flex-grow pt-1 overflow-y-auto" ref={measuredRef}>
<li className="min-h-16 flex justify-center items-center">*</li>
<li className="min-h-16 flex justify-center items-center">*</li>
<li className="min-h-16 flex justify-center items-center">*</li>
<li className="min-h-16 flex justify-center items-center">*</li>
<li className="min-h-16 flex justify-center items-center">*</li>
<li className="min-h-16 flex justify-center items-center">*</li>
<li className="min-h-16 flex justify-center items-center">*</li>
<li className="min-h-16 flex justify-center items-center">*</li>
// ........ more message .........
</ul>
);
};

Focus the last element of a map html rendering

In my react typescript project, I have a component which generates multiple div with the same id when I map through it.
<div id="projectName" ref={ref} contentEditable="true" className="font-semibold focus:outline-none resize-none">{projectTitle}</div>
<div id="projectName" ref={ref} contentEditable="true" className="font-semibold focus:outline-none resize-none">{projectTitle}</div>
<div id="projectName" ref={ref} contentEditable="true" className="font-semibold focus:outline-none resize-none">{projectTitle}</div>
My goal is to set the focus on the last element of it when I click on my button. For that, I tried to use a method I found on another stack, but I can't have access to the focus() method like they use in the post.
I don't really know how I could process this.
var size = document.querySelectorAll("[id=projectName]")
var last = size[size.length - 1]
last.focus() // error here
The error is the following one
Property 'focus' does not exist on type 'Element'.
Instead of using querySelector(), please try to use refs when it comes to react. I see you are trying to do that but didn't implement fully.
export default function App() {
let list = [1, 2, 3, 4, 5];
let elRefs = useRef([]);
useEffect(() => {
let size = elRefs.current.length;
elRefs.current[size - 1].focus();
});
return (
<div className="App">
{list.map((x) => (
<div
id="projectName"
tabIndex="1"
ref={(el) => (elRefs.current = [...elRefs.current, el])}
key={x}
contentEditable="true"
className="font-semibold focus:outline-none resize-none"
>
x
</div>
))}
</div>
);
}
Pass in a function to ref and keep appending to the .current array. And yes as mentioned you have to use tabIndex to make non focusable items focusable.
Code sandbox link

Tailwind conditional transition

I'm creating a collapse/accordion component that shows the content when clicked. I want to add a little delay to that action but what I've doesn't seem to work. Here is what I have:
const Accordion = ({ children, open }) => {
const contentSpace = useRef(null);
return (
<div className="flex flex-col">
<div
ref={contentSpace}
className={clsx(
'overflow-auto overflow-y-hidden h-0 transition-height duration-700 ease-in-out',
{
'h-full': open,
}
)}
>
<div className="py-2">{children}</div>
</div>
</div>
);
};
Any ideas?
Seems like you have to use numeric values in order to make the animation work. So I had to change h-full with a numeric height.

Resources