How to invert animation order in react-spring's useTransitions - reactjs

I want to animate the opacity of the items from number 1 to 4, but want to run it inverted (from 4 to 1) if the items are removed. I thought that the reverse flag could help, but it doesn't do anything:
import React, { useState } from "react";
import { animated, config, useTransition } from "react-spring";
export default function App() {
const items = [1, 2, 3, 4];
const [isToggled, setToggled] = useState(false);
const transitions = useTransition(isToggled ? items : [], item => item, {
config: config.gentle,
unique: true,
trail: 250,
reverse: isToggled ? false : true,
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }
});
return (
<div className="App">
<button onClick={() => setToggled(!isToggled)}>Toggle</button>
{transitions.map(({ item, key, props }) => (
<animated.div key={key} style={props}>
Issue #{item}
</animated.div>
))}
</div>
);
}
CodeSandbox

The problem with reverse method is it reverse all the content inside the array.
You only need to reverse the props properties inside the result of your useTransition.
With simple array modification like this (in typescript) :
// utils/animation.ts
// or js just modify the type
import { UseTransitionResult } from 'react-spring';
export function reverseTransition<T, Result extends UseTransitionResult<T, object>>(
arr: Result[],
): Result[] {
const result: Result[] = [];
for (let idx = 0; idx < arr.length; idx++) {
result.push({
...arr[idx],
props: arr[arr.length - 1 - idx].props,
});
}
return result;
}
and pass the result of useTransition hooks like this :
import React, { useState } from "react";
import { animated, config, useTransition } from "react-spring";
// import above code
import { reverseTransition } from "utils/animation";
export default function App() {
const items = [1, 2, 3, 4];
const [isToggled, setToggled] = useState(false);
const transitions = useTransition(isToggled ? items : [], item => item, {
config: config.gentle,
unique: true,
trail: 250,
reverse: isToggled ? false : true,
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }
});
return (
<div className="App">
<button onClick={() => setToggled(!isToggled)}>Toggle</button>
{(isToggled ? transitions : reverseTransition(transitions)).map(({ item, key, props }) => (
<animated.div key={key} style={props}>
Issue #{item}
</animated.div>
))}
</div>
);
}
You will get the reversed animation with the same content.
I hope it helps!
Codesandbox
Notes: I am using React Spring v8, not v9 (the one that you use in your Codesandbox)
Regards

Related

I use UseTransition of react-spring. The whole list animates when I need only one item to be animated. What`s problem?

my problem is ItemList animation, every time when i change an item - delete for example react renders and animates the whole itemList which is unnexpected behavior
Also i`d like my items to be animated when only delete and create items, im using react-spring library
But there is also an interesting thing. If i delete items from the lowest to up gradually it works as expected but if i delete elements from top to bottom the list of items rerenders and animates fully and i don`t unredstand why.
HomePage:
import PostForm from '../components/PostForm/PostForm';
import {MemoizedToDoList} from '../components/ToDoList/ToDoList';
import { useGetToDosQuery } from '../store/rtcApi';
const HomePage = () => {
const data = useGetToDosQuery();
return (
<div>
<div className="ToDoMain">
<PostForm/>
<MemoizedToDoList items={data.data? data.data.toDos.toDos:[]} isLoading={data.isLoading}/>
</div>
</div>
)
}
export default HomePage;
ToDoList:
import React from 'react'
import { useGetToDosQuery } from '../../store/rtcApi';
import { useSelector } from 'react-redux';
import { useTransition } from 'react-spring';
import LoadingSpinner from "../LoadingSpinner/LoadingSpinner";
import ToDoItem from '../ToDoItem/ToDoItem'
import ToDoListCss from "./ToDoList.module.css";
const ToDoList = ({items, isLoading}) => {
const {toDos} = useSelector(state => state.main);
const {isAuth} = useSelector(state => state.auth);
let toDosData = [];
if(isAuth && items){
toDosData = items;
}else{
toDosData = toDos;
}
const transition = useTransition(toDosData, {
from: {x: -100, y:800, opacity: 0},
enter: {x: 0, y:0, opacity: 1},
leave: {x: 100, y: 800, opacity: 0},
keys: item => item.id,
trail: 300
});
if(isLoading)
return <LoadingSpinner scaleSet={0.5}/>;
return (
<div className={ToDoListCss.toDoList}>
{transition((style, item, key)=><ToDoItem style={style} item={item} key={key}/>)}
</div>
)
}
export const MemoizedToDoList = React.memo(ToDoList);
ToDoItem:
import React from 'react'
import { useDispatch, useSelector } from 'react-redux';
import { useRemoveToDoMutation } from "../../store/rtcApi";
import { removeToDo } from "../../store/slices/mainSlice";
import {useSpring, animated} from "react-spring";
import { BsPatchExclamationFill } from 'react-icons/bs';
import { RiDeleteBin2Line } from "react-icons/ri";
import ToDoItemCss from "./toDoItem.module.css";
const ToDoItem = ({item, style}) => {
const dispatch = useDispatch();
const {isAuth} = useSelector((state)=>state.auth);
const [ removeUserToDo ] = useRemoveToDoMutation();
const crossLineStyle = useSpring({
to: { opacity: 1, width: "65%", transform:"rotate(8deg)" },
from: { opacity: 0, width: "0%", transform:"rotate(-20deg)" },
reverse: !item.isComplete,
config: { frequency: 0.1 }
});
const onRemoveItem = React.useCallback((item) => {
if(isAuth){
return removeUserToDo(item._id);
}
dispatch(removeToDo(item.id));
}, [dispatch])
return (
<animated.div style={style} className={ToDoItemCss.toDoItem}>
<animated.div style={crossLineStyle} className={ToDoItemCss.overCrossLineAnimated}></animated.div>
<div className={ToDoItemCss.toDoItemText}>{item.title}</div>
<div className={ToDoItemCss.todoItemIconGroup}>
{item.isImportant && <div className={ToDoItemCss.todoItemImportantIcon}><BsPatchExclamationFill/></div>}
<div onClick={()=>{onRemoveItem(item)}} className='todo-item_bin-icon'><RiDeleteBin2Line/></div>
</div>
</animated.div>
)
}
export default ToDoItem;
I was trying to use memo and useCallBack but i think i don`t get how shoud i properly use it here with the RTC query and redux state.
Chunks of code from ToDoList:
const transition = useTransition(toDosData, {
from: {x: -100, y:800, opacity: 0},
enter: {x: 0, y:0, opacity: 1},
leave: {x: 100, y: 800, opacity: 0},
keys: item => item.id,
trail: 300
});
if(isLoading)
return <LoadingSpinner scaleSet={0.5}/>;
return (
<div className={ToDoListCss.toDoList}>
{transition((style, item, key)=><ToDoItem style={style} item={item} key={key}/>)}
</div>
)
export const MemoizedToDoList = React.memo(ToDoList);
and here i have used useCallback and i even dono why =))
ToDoItem
const onRemoveItem = React.useCallback((item) => {
if(isAuth){
return removeUserToDo(item._id);
}
dispatch(removeToDo(item.id));
}, [dispatch])
Here how it looks like

Change attribute from object using requestAnimationFrame in React

I trying to update human.x using requestAnimationFrame and useState. I want to add elements called "humans" and use requestAnimationFrame to update the position to humans walk in "world" (using css properties). The method addHumans work but requestAnimationFrame update and empty my list.
World.tsx
import { useState, useEffect } from 'react'
import Human from './Human'
import IHuman from './Human'
type IHuman = {
id: number;
x: number;
y: number;
color: string;
}
export default function World() {
const [humans, setHumans] = useState<IHuman[]>([])
const addHuman = () => {
setHumans([...humans, {id: Math.random(), x: 0, y: 0, color: 'red'}])
}
const animate = () => {
setHumans(humans?.map((human, i) => {
return {...human, x: human.x + 1}
}));
// console.log(humans)
requestAnimationFrame(animate);
}
useEffect(() => {
requestAnimationFrame(animate);
}, []); // run once
return (
<>
<button onClick={addHuman}>new human</button>
<main>
{humans?.map(human => {
return <Human key = {human.id} x = {human.x} y = {human.y} color = {human.color} />
})}
</main>
</>
)
}
Human.tsx
export default function Human(props: any) {
return (
<div key={props.id} className="human" style={{ top: props.y + 'px', left: props.x + 'px', backgroundColor: props.color }}>{props.id}</div>
)
}
This is called a stale state. Update your code like this:
setHumans(humans => humans?.map((human, i) => {
return {...human, x: human.x + 1}
}));

useTransition mounts new object instantly

I am trying to figure out how to utilise useTransition for page transitions (simple opacity change where first page fades out and new one fades in).
So far I have this small demo going https://codesandbox.io/s/sleepy-knuth-xe8e0?file=/src/App.js
it somewhat works, but weirdly. When transition starts new page is mounted instantly while old one starts animating. This causes various layout issues and is not behaviour I am after. Is it possible to have first element fade out and only then mount and fade in second element?
Code associated to demo
import React, { useState } from "react";
import "./styles.css";
import { useTransition, a } from "react-spring";
export default function App() {
const [initial, setInitial] = useState(true);
const transition = useTransition(initial, {
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }
});
return (
<div>
{transition((style, initial) => {
return initial ? (
<a.h1 style={style}>Hello Initial</a.h1>
) : (
<a.h1 style={style}>Hello Secondary</a.h1>
);
})}
<button onClick={() => setInitial(prev => !prev)}>Change Page</button>
</div>
);
}
you can delay the start of the transition by waiting for the leave animation to complete.
const sleep = t => new Promise(res => setTimeout(res, t));
...
const transition = useTransition(initial, {
from: { position: "absolute", opacity: 0 },
enter: i => async next => {
await sleep(1000);
await next({ opacity: 1 });
},
leave: { opacity: 0 }
});
This delays the animation also for the very first time it is run. You can have a ref to keep track of whether the component has been rendered before or if it is its first time rendering, then you can skip sleep call if it's the first render.
OR
You can just simply provide trail config
const transition = useTransition(initial, {
from: { position: "absolute", opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
trail: 300
});
You need to add position: absolute and then you need to set the right position with css.
import React, { useState } from "react";
import "./styles.css";
import { useTransition, a } from "react-spring";
export default function App() {
const [initial, setInitial] = useState(true);
const transition = useTransition(initial, {
from: { position: 'absolute', opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }
});
return (
<div>
{transition((style, initial) => {
return initial ? (
<a.h1 style={style}>Hello Initial</a.h1>
) : (
<a.h1 style={style}>Hello Secondary</a.h1>
);
})}
<button onClick={() => setInitial(prev => !prev)}>Change Page</button>
</div>
);
}

How to test mousemove drag and drop with react-testing-library and framer-motion

I am trying to test the drag and drop functionality using react-testing-libary. The drag and drop functionality comes from framer-motion and the code is in reacy. From what I understand it uses the mousedown, mousemove and mouseup events to do this. I want to test drag and drop functionality of the following basic component:
export const Draggable: FC<DraggableInterface> = ({
isDragging,
setIsDragging,
width,
height,
x,
y,
radius,
children,
}) => {
return (
<motion.div
{...{ isDragging }}
{...{ setIsDragging }}
drag
dragConstraints={{
left: Number(`${0 - x}`),
right: Number(
`${width - x}`,
),
top: Number(`${0 - y}`),
bottom: Number(
`${height - y}`,
),
}}
dragElastic={0}
dragMomentum={false}
data-test-id='dragabble-element'
>
{children}
</motion.div>
);
};
And I have a snippet of the test as follows:
it('should drag the node to the new position', async () => {
const DraggableItem = () => {
const [isDragging, setIsDragging] = useState<boolean>(true);
return (
<Draggable
isDragging={isDragging}
setIsDragging={() => setIsDragging}
x={0}
y={0}
onUpdateNodePosition={() => undefined}
width={500}
height={200}
>
<div
style={{
height: '32px',
width: '32px'
}}
/>
</Draggable>
);
};
const { rerender, getByTestId } = render(<DraggableItem />);
rerender(<DraggableItem />);
const draggableElement = getByTestId('dragabble-element');
const { getByTestId, container } = render(
<DraggableItem />
);
fireEvent.mouseDown(draggableElement);
fireEvent.mouseMove(container, {
clientX: 16,
clientY: 16,
})
fireEvent.mouseUp(draggableElement)
await waitFor(() =>
expect(draggableElement).toHaveStyle(
'transform: translateX(16px) translateY(16px) translateZ(0)',
),
);
However, I cannot get the test to pass successfully as the transform value I test for is set to none. It does not update it the value with the updated CSS. I think there is some sort of async issue or animation delay so the mousemove is not detected and the value of the transform does not change. Would anyone know how to get the test to work or a way to test the mousemove changes?
Any advice or guidance on how I can solve this would be greatly appreciated!
It looks like you are invoking mouseMove() on the container instead of your draggable item. The container here refers to a root div containing your DraggableItem but is not the item itself (API). Therefore, events are being fired on the root div and not the item.
Here is a simple working example for testing draggable elements (for passers-by looking to test mouse down, move, and up events on draggable elements):
//
// file: draggable-widget.tsx
//
import $ from 'jquery'
import * as React from 'react'
type WidgetProps = { children?: React.ReactNode }
export default class DraggableWidget extends React.Component<WidgetProps> {
private element: React.RefObject<HTMLDivElement>
constructor(props: WidgetProps) {
super(props)
this.element = React.createRef()
}
show() { if (this.element.current) $(this.element.current).show() }
hide() { if (this.element.current) $(this.element.current).hide() }
getLocation() {
if (!this.element.current) return { x: 0, y: 0 }
return {
x: parseInt(this.element.current.style.left),
y: parseInt(this.element.current.style.top)
}
}
private onDraggingMouse(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
let location = this.getLocation()
let offsetX = e.clientX - location.x
let offsetY = e.clientY - location.y
let mouseMoveHandler = (e: MouseEvent) => {
if (!this.element.current) return
this.element.current.style.left = `${e.clientX - offsetX}px`
this.element.current.style.top = `${e.clientY - offsetY}px`
}
let reset = () => {
window.removeEventListener('mousemove', mouseMoveHandler)
window.removeEventListener('mouseup', reset)
}
window.addEventListener('mousemove', mouseMoveHandler)
window.addEventListener('mouseup', reset)
}
render() {
return (
<div ref={this.element} className="draggable-widget">
<div className="widget-header"
onMouseDown={e => this.onDraggingMouse(e)}>
<button className="widget-close" onClick={() => this.hide()}
onMouseDown={e => e.stopPropagation()}></button>
</div>
</div>
)
}
}
Then for the test logic:
//
// file: draggable-widget.spec.tsx
//
import 'mocha'
import $ from 'jquery'
import * as React from 'react'
import { assert, expect } from 'chai'
import { render, fireEvent } from '#testing-library/react'
import Widget from './draggable-widget'
describe('draggable widget', () => {
it('should move the widget by mouse delta-xy', () => {
const mouse = [
{ clientX: 10, clientY: 20 },
{ clientX: 15, clientY: 30 }
]
let ref = React.createRef<Widget>()
let { container } = render(<Widget ref={ref} />)
let element = $(container).find('.widget-header')
assert(ref.current)
expect(ref.current.getLocation()).to.deep.equal({ x: 0, y: 0 })
fireEvent.mouseDown(element[0], mouse[0])
fireEvent.mouseMove(element[0], mouse[1])
fireEvent.mouseUp(element[0])
expect(ref.current.getLocation()).to.deep.equal({
x: mouse[1].clientX - mouse[0].clientX,
y: mouse[1].clientY - mouse[0].clientY
})
})
})
Found this section in the react-testing-library docs
https://testing-library.com/docs/dom-testing-library/api-events/#fireeventeventname
Scroll down to the dataTransfer property section - apparently this is what we should be using to test drag-and-drop interactions

useTransition with react-spring as a component changes

I'm attempting to animate a card in and out. If there is a selected value, the card appears. If the selected item is undefined, the card disappears. I got this to work.
The next thing I tried to do is make it that if the selection changed (A new item) - animate out a card and animate in a new one. I'm confused on how to make this work... here is what I've attempted that kind of works.
Clearly I'm not understanding how this should be done. I'm wondering if I need to break this up into two cards and run useChain.
const App: React.FC = () => {
//...
const [selectedItem, setSelectedItem] = useState<TimelineItem | undefined>(undefined);
const [lastSelectedItem, setLastSelectedItem] = useState<TimelineItem>({
content: '',
start: new Date(),
id: 0,
});
//...
const transitions = useTransition(
[selectedItem, lastSelectedItem],
item => (item ? item.id : 0),
{
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
}
);
return (
<Timeline
onItemSelect={item => {
if (selectedItem) setLastSelectedItem(selectedItem);
setSelectedItem(item);
}}
/>
{transitions.map(({ item, key, props }) => {
return (
item && (
<animated.div style={props}>
{item === selectedItem ? (
<ItemDetails
item={selectedItem} // If the selected item is undefined, this will not be running (happens when unselecting something)
groups={groups}
key={key || undefined} // key becomes undefined since item is
></ItemDetails>
) : (
false && ( // The last item never shows, it still has the data for the lastSelectedItem (For the fade out while the new Item is being shown or there is no new item).
<ItemDetails
item={lastSelectedItem}
groups={groups}
key={key || undefined}
></ItemDetails>
)
)}
</animated.div>
)
);
})}
);
};
If I understand you well, you want to display the state of an array. New elements fade in and old one fades out. This is the functionality the Transition created for. I think it can be done a lot simpler. I would change the state managment and handle the array in the state. And the render should be a lot simpler.
UPDATE:
I created an example when the animation of the entering element wait for the animation of the leaving element to finish.
I made it with interpolation. The o value changes from 0 to 1 for enter, and 1 to 2 for leave. So the opacity will change:
leave: 1 -> 0 -> 0
enter: 0 -> 0 -> 1
Here is the code:
import React, { useState, useEffect } from "react";
import { useTransition, animated } from "react-spring";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [cards, set] = useState(["A"]);
useEffect(() => {
setInterval(() => {
set(cards => (cards[0] === "A" ? "B" : "A"));
}, 4000);
}, []);
const transitions = useTransition(cards, null, {
from: { o: 0 },
enter: { o: 1 },
leave: { o: 2 },
config: { duration: 2000 }
});
return transitions.map(({ item, key, props }) => (
<div style={{ fontSize: "300px" }}>
<animated.div
style={{
position: "absolute",
opacity: props.o.interpolate([0, 0.5, 1, 1.5, 2], [0, 0, 1, 0, 0])
}}
>
{item}
</animated.div>
</div>
));
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
working example: https://codesandbox.io/s/react-spring-staggered-transition-xs9wy

Resources