GSAP Tween taking longer on each play in React - reactjs

I am using GSAP's timeline to animate elements and it looks like it's taking longer and longer each time. In the example below, you can click on the box to animate it, and then click to reverse it. You can see in my setup that I don't have any delays set. If you open the console you will see the log takes longer and longer to execute the message in the onComplete function.
From research I've done, it looks like I am somehow adding a Tween, but I can't figure out how to solve this. Any help would be greatly appreciated. CodePen here.
const { useRef, useEffect, useState } = React
// set up timeline
const animTimeline = gsap.timeline({
paused: true,
duration: .5,
onComplete: function() {
console.log('complete');
}
})
const Box = ({ someState, onClick }) => {
const animRef = useRef();
animTimeline.to(animRef.current, {
x: 200,
})
useEffect(() => {
someState ? animTimeline.play() : animTimeline.reverse();
}, [someState])
return (
<div
className="box"
onClick={onClick}
ref={animRef}
>
</div>
)
}
const App = () => {
const [someState, setSomeState] = useState(false);
return(
<Box
someState={someState}
onClick={() => setSomeState(prevSomeState => !prevSomeState)}
/>
);
}
ReactDOM.render(<App />,
document.getElementById("root"))

Issue
I think the issue here is that you've the animTimeline.to() in the component function body so this adds a new tweening to the animation each time the component is rendered.
Timeline .to()
Adds a gsap.to() tween to the end of the timeline (or elsewhere using
the position parameter)
const Box = ({ someState, onClick }) => {
const animRef = useRef();
animTimeline.to(animRef.current, { // <-- adds a new tween each render
x: 200,
})
useEffect(() => {
someState ? animTimeline.play() : animTimeline.reverse();
}, [someState])
return (
<div
className="box"
onClick={onClick}
ref={animRef}
>
</div>
)
}
Solution
Use a mounting effect to add just the single tweening.
const animTimeline = gsap.timeline({
paused: true,
duration: .5,
onComplete: function() {
animTimeline.pause();
console.log('complete');
},
onReverseComplete: function() {
console.log('reverse complete');
}
})
const Box = ( { someState, onClick }) => {
const animRef = useRef();
useEffect(() => {
animTimeline.to(animRef.current, { // <-- add only one
x: 200,
});
}, []);
useEffect(() => {
someState ? animTimeline.play() : animTimeline.reverse();
}, [someState])
return (
<div
className="box"
onClick={onClick}
ref={animRef}
/>
)
};
Demo

Related

Can't get an example for React useMutationObserver hook to run

I need to check the mutation of an react component in my app that I am developing. So I looked around for solutions for that and found this example. To learn how it works I am trying to implement it in my own app: https://www.30secondsofcode.org/react/s/use-mutation-observer
I have created a blank new React app but I can't get it to run like it does in the codepen provided on the site.
1st. ref is missing as a dependency in the useEffect hook, so I added it.
2nd. It does nothing, it updates the text output in <p>{content}</p> but it keeps staying on mutationCount: 0
Here my App.js
import React from "react";
const useMutationObserver = (
ref,
callback,
options = {
attributes: true,
characterData: true,
childList: true,
subtree: true,
}
) => {
React.useEffect(() => {
if (ref.current) {
const observer = new MutationObserver(callback);
observer.observe(ref.current, options);
return () => observer.disconnect();
}
}, [callback, options, ref]); //added ref here
};
const App = () => {
const mutationRef = React.useRef();
const [mutationCount, setMutationCount] = React.useState(0);
const incrementMutationCount = () => {
return setMutationCount(mutationCount + 1);
};
useMutationObserver(mutationRef, incrementMutationCount);
const [content, setContent] = React.useState('Hello world');
return (
<>
<label htmlFor="content-input">Edit this to update the text:</label>
<textarea
id="content-input"
style={{ width: '100%' }}
value={content}
onChange={e => setContent(e.target.value)}
/>
<div
style={{ width: '100%' }}
ref={mutationRef}
>
<div
style={{
resize: 'both',
overflow: 'auto',
maxWidth: '100%',
border: '1px solid black',
}}
>
<h2>Resize or change the content:</h2>
<p>{content}</p>
</div>
</div>
<div>
<h3>Mutation count {mutationCount}</h3>
</div>
</>
);
};
export default App;
What is different in my app?
A new instance of callback and options is created every time App component re-renders, making the useEffect callback function running on every change. You can remove them from the dependency list and make sure that useEffect block will run after component is mounted or if ref changes only:
const useMutationObserver = (
ref,
callback,
options = {
CharacterData: true,
childList: true,
subtree: true,
attributes: true,
}
) => {
React.useEffect(() => {
if (ref.current) {
const observer = new MutationObserver(callback);
observer.observe(ref.current, options);
return () => observer.disconnect();
}
}, [ref]);
};
Working Example
Another solution is to keep callback and options reference with no changes.

Change background color after first rendering, but the background transition animations sometimes don't show

Render 5 Mid components.
export default () => {
return (
<>
<Mid />
<Mid />
<Mid />
<Mid />
<Mid />
</>
);
};
Mid is to get the data and only show the Block if the data is not empty
const Mid = () => {
const [data, setData] = useState(null);
useEffect(() => {
const t = setTimeout(() => {
setData([]);
}, 500);
return () => {
clearInterval(t);
};
}, []);
if (data) {
return <Block />;
}
return null;
};
Block has set css transition. After it first rendering, changed the background color in useEffect.
const Block = () => {
const eleRef = useRef();
useEffect(() => {
eleRef.current.style.background = "green";
}, []);
return (
<span
ref={eleRef}
style={{
transition: "background 5s",
background: "#ccc",
margin: 4,
}}
/>
);
};
I hope to see every Block has the background transition animations when it first rendered, but some of them has no animation.
Here is the online demo and gif.
codesandbox demo
What is the cause?

How do i implement react-dnd-text-dragpreview in a Functional Component?

i'm using react-dnd and i'm able to put an image when draging, but now i would like to instead of a image i want a custom text.
i found this component react-dnd-text-dragpreview, but the example is for react class component.
i've tried to put "dragPreviewImage" in the src of "DragPreviewImage" , but doesn't work.
can someone help me on this ?
thanks in advance !
https://www.npmjs.com/package/react-dnd-text-dragpreview
sample code
...
import { DragPreviewImage, useDrag } from 'react-dnd';
import { boxImage } from '../components/boxImage';
import { createDragPreview } from 'react-dnd-text-dragpreview'
function FieldDrag({ field, dropboxField, onDragEnd = () => null, setFieldValue = () => null, cargoCategories }) {
const [{ isDragging }, drag, preview] = useDrag(() => ({
type: 'field',
item: { id: field.id, dragCargoInfoId: field.dragCargoInfoId, dragCargoInfo: field.childDragCargoInfo },
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
end: (item) => endDrag(item),
}));
const endDrag = (item) => {
onDragEnd(item);
};
const styles = {
fontSize: '12px'
}
const dragPreviewImage = createDragPreview('Custom Drag Text', styles);
.....
return (
<>
<DragPreviewImage connect={preview} src={boxImage} />
<span ref={drag} className="flex-item" style={{ ...computedStyle, ...styleDropboxField }}>
{getField(field, extraStyle, isboxed, cargoCategories)}
</span>
</>
);
drag with image
I found the solution in the codesandbox --> https://codesandbox.io/s/uoplz?file=/src/module-list.jsx:2522-2619
just put src image "dragPreviewImage.src".
<DragPreviewImage connect={preview} src={dragPreviewImage &&
dragPreviewImage.src} />

React spring useTransition state updates modifying exiting component

I'm using react-spring to animate transitions in a list of text. My animation currently looks like this:
As you can see, the text in the exiting component is also updating, when I would like it to stay the same.
Here's what I am trying:
import {useTransition, animated} from 'react-spring'
import React from 'react'
function useInterval(callback, delay) {
const savedCallback = React.useRef();
// Remember the latest callback.
React.useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
React.useEffect(() => {
let id = setInterval(() => {
savedCallback.current();
}, delay);
return () => clearInterval(id);
}, [delay]);
}
function App() {
const [copyIndex, setCopyIndex] = React.useState(0);
const transitions = useTransition(copyIndex, null, {
from: { opacity: 0, transform: 'translate3d(0,100%,0)', position: 'absolute'},
enter: { opacity: 1, transform: 'translate3d(0,0,0)' },
leave: { opacity: 0, transform: 'translate3d(0,-50%,0)' }
});
const copyList = ["hello", "world", "cats", "dogs"];
useInterval(() => {
setCopyIndex((copyIndex + 1) % copyList.length);
console.log(`new copy index was ${copyIndex}`)
}, 2000);
return (
transitions.map(({ item, props }) => (
<animated.div style={props} key={item}>{copyList[copyIndex]}</animated.div>
))
)
}
export default App;
Any ideas on how to get this to work as desired? Thank you so much!
Let the transition to manage your elements. Use the element instead of the index. Something like this:
const transitions = useTransition(copyList[copyIndex], item => item, {
...
transitions.map(({ item, props }) => (
<animated.div style={props} key={item}>{item}</animated.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

Resources