Animate movement of React component from one to another - javascript

I'm trying to figure out how to animate moving react component from one to another. For example take very simple, yet interesting card game: you may place any card to a deck, or take top card from the deck.
To make it work, I have 4 classes - <Board> which holds two <Card Collection>: "Deck" and "Hand" components. In constructor, they generate CardModel items and render them via <Card> component. <CardCollection> component has special onCardClick prop which takes callback function. In my case it's onCardClick={/*this is equal to <Board>*/ this.transferCard("Hand")}
Board.transferCard takes clicked CardModel from state of one component and pushes it to another.
The problem is animation - I want card to fly, preferably through center (optional!) from old place to new. I am able to place the newly created Card in "new place" to the same place as old component, but, since i jsut strated to learn React, I'm not sure where exactly I should start. Wrap in ReactCSSTransitionGroup? pass "animate: from{x,y} to{x,y}" property to <CardCollection>?
So the full question is what is the most generic, controllable and "react" way to animate this situation?
JSFiddle base question version: https://jsfiddle.net/fen1kz/6qxpzmm6/
JSFiddle first animation prototype version: https://jsfiddle.net/fen1kz/6qxpzmm6/1
I don't like <this.state.animations.map... here. Also the Animation component looks like an abomination for me, I'm not sure this is the good architecture style for React.

The main mistake I did is try to mix render function with animation. No! Render should not contain anything related to animation (preferably not even starter values), while all the animation should happen after render. The only thing that bothers me is that i still should have state of animations in CardCollection just to throw it into creation of Card
Working example: https://jsfiddle.net/fen1kz/6qxpzmm6/4/
let animation;
if (this.animations[cardModel.id] != void 0) {
animation = this.animations[cardModel.id];
this.animations[cardModel.id] = void 0;
}
...
return <Card
cardModel={cardModel}
onCardClick={onCardClick}
animation={animation}
position={this.getCardPosition(i)}
index={i}
key={cardModel.id}
ref={cardModel.id} // prolly not needed
/>

You can try to use a package I made called react-singular-component, which might actually do what you need.
The component allows you to render a component server times and by a given priority only the highest one will render. Mounting and uncounting the given component will cause the next highest priority to render instead with an animation moving and wrapping the component from its last place and size to the new one.
Here is the Github page: https://github.com/dor6/SingularComponent

Just an idea: You could try to use jQuery animate for animation of moving some HTML element from one place to another. Once animation is complete there is a complete function property only then you could trigger your onCardClick.

Related

Why are the state/css transition times not identical for this simple slider made with ReactJS?

I made a functional demo sandbox here
This is a basic array cycler with 3 elements. And these 3 elements are rendered as slides which move visually left/right depending on the direction you pick.
I don't think the approach I took to make this work is a good one, and if you have suggestions on that I'm open to it.
But the actual question, in order to make the sliding work "equivalently" in both direction i.e. left/right I had to delay the one "sliding" to the left. My naming convention is kind of confusing too because you click the button e.g. prev/next and the array cycling/sliding is flipped. I did use an anti-pattern with the external variable that holds the direction selected since I was having problems with multiple states affecting the slider/causing rendering issues.
But TL:DR this is the onClick handler for the prev/next buttons passing in boolean for direction.I'm using a CSS animation for the motion part. I'm also aware nested ternaries are bad.
const cycleArr = cyclePrev => {
if (!slideDone) {
return;
}
setSlideDone(false);
const newArrSort = cyclePrev ? cycleLeft(slides) : cycleRight(slides);
slideClassRef.current.classList = slideDir
? `App ${slideDir === "left" ? "slide-left" : "slide-right"}`
: "App";
if (slideDir === "left") {
setSlides(newArrSort);
setTimeout(() => {
slideClassRef.current.classList = "App";
setSlideDone(true);
}, 1050);
} else {
setTimeout(() => {
slideClassRef.current.classList = "App";
setSlides(newArrSort);
setSlideDone(true);
}, 1000);
}
};
I'm aware I could have just used something off the shelf eg. slick carousel but this is a good demo of my current problems with state planning in ReactJS. I'm trying to get better/think better at it.
I'm not sure I quite understand your question, if you are referring to the additional 50ms delay when sliding, my best guess is that the call to setSlides(newArrSort); sets the state and also triggers an immediate React re-render of the component. This probably takes some amount of time, hence the desynchronisation with the CSS transition.
Anti-patterns are not there to make your life difficult, they are there to stop you getting into a confusing mess :)
Components can re-render whenever React deems it necessary, which is why state should be properly stored, and pretty effects done in useEffect hooks. I would recommend a more React-based data flow, where you update the state at the top, and let it propagate downwards, reacting to the new state, applying the correct CSS transformations. It's declarative, like HTML and CSS. You don't tell the browser what to paint, you describe how to paint it.

Problem re-using a Class / Not understanding instantiation

Update - JSFiddle demo here: https://jsfiddle.net/vb2ptmuo/11/
I'm playing around with an interesting effect I came across: https://tympanus.net/codrops/2019/08/07/image-trail-effects/
To start, I have a div containing a bunch of img elements. It dumps them into an array and then creates a trail effect from those images which follows the mouse. You kick this all off via new ImageTrail(".content"). But what if I have more than one set of images and I want to re-trigger it again with those, instead? Example:
<div class="content">
<img src="1.jpg">
<img src="2.jpg">
<img src="3.jpg">
</div>
<div class="content-2">
<img src="4.jpg">
<img src="5.jpg">
<img src="6.jpg">
</div>
Doing a second new ImageTrail(".content-2") does not replace the first set of images with the second set, even though the code reads to me like it should. You still just see the first set of images in the trail.
I'm also slightly concerned with performance if I'm instantiating the ImageTrail class twice (if that's even a thing), but this is wholly secondary to my main issue.
I feel like there's a simple solution but I'm missing it. Scroll down to the bottom of the demo for the commented code "This doesn't work"
The main cause is requestAnimationFrame as part of constructor and render method.
Across instances, for this use case, one instance should have the rendering control using requestAnimationFrame.
I did a trick here, for this use case.
Every time a new instance is created, it will cancel earlier instance's render request (done by requestAnimationFrame) by means of cancelAnimationFrame
Even i cancelled the request done in render method too.
Check out this jsfiddle for modified code : https://jsfiddle.net/rahultarafdar/mfboqy9g/45/
The Code that you posted has one major problem: it uses global variables to store state of the ImageTrail object. This is a pretty bad programming practice as it leads to unexpected behaviour and defeats the purpose of having a class in the first place.
The reason why only the first set of pictures is shown is because both instances of ImageTrail register a callback for the next animation frame with requestAnimationFrame. The callback (render) of the first instance is always called first, because it registered the listener first. This first render function updates the global mousePos variables. The second render function thinks the mouse has not moved, because lastMousePos and mousePos are the same.
Now to address your problem: first you should move all the state that is used by ImageTrail into the class itsef. That includes mousePos, cacheMousePos and lastMousePos. If you have done that successfully, you should get both imagesets diyplaying at the same time, if you have two instances. To activate/deactivate the rendering of ImageTrails, you could add an active attribute, which gets checked at every render call or you could implement methods that cancel/setup the animation frame for a specific instance. (For details how to use cancelAnimationFrame you could look into the answer from rahul or into the MDN)

My React component does not update in the Safari browser

Thought everything was going great. Works perfectly in Chrome, FF, Edge, even IE 11!
The parent component holds all the sate. I pass the bets object to the child component which calculates a count to pass to the grandchild component to display.
The parent state 'bets' is an object with the keys as an ID and the value as an object.
The parent state is correctly changing when I interact with the app. Why will only Safari not update when the parent state changes? (on iOS and MacOS)
Parent
class ParentComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
bets: {}
};
}
}
Child
getBadgeCount = (league) => {
const bets = this.props.bets;
let count = 0;
Object.keys(bets).map((bet) => bets[bet].event.league === league && count++);
return count;
};
// ...
<ChildItem count={this.getBadgeCount(league)} />
GrandChild
class GrandChildComponent extends React.Component {
render() {
const { count } = this.props;
return (
<div>
<div>
{count > 0 && <div>{count}</div>}
</div>
</div>
);
}
}
I console.log the count inside the grandchild render and in the componentDidUpdate and it shows the right number. Is there something apple/safari specific with react I am missing?
In the case where there are variables being changed based on which you can switch between the styles (opacity: 1 or opacity: 0.99) You can try adding a key to the element which is not being updated in Safari/iOS.
<div key={new Date()} className={'myComponent'}>{Count}</div>
I ran into the same problem, this seems to work for now.
So I did solve the very bizarre issue after a few days.
What was happening was I guess the engine in the Safari browser was not re-rendering the little badge icon correctly. I would print out the value inside the DOM, OUTSIDE of the styled badge I was using, and the number would update as expected... Leading me to believe the issue was related to styles.
THEN after removing what I thought was the CSS causing the issue, I noticed that it looked like safari was 'partially' updating the value in the badge. It appears to have half re-rendered, and the previous number collides with the new value... Not sure if this is some obscure problem with the render engine?
After adding and removing all the CSS one by one, the issue remained so I decided to trick the browser to 'force' render with a simple calculation inside the grandchild where it was being rendered:
const safariRenderHack = { opacity: count % 2 ? 1 : 0.99 };
<div style={safariRenderHack}>{count}</div>
Not a great solution but oh well I guess it's fixed. ha
I just run into similar problem.
Safari didn't re-render some texts implemented as:
<span>{someValue}</span>
On 'someValue' field update, reactJS worked fine (element was requested to render) but Safari re-renders only area of new value (shorter than previous). UI glitches :-/
If I done anything to CSS via Developer tools, element has been rendered again and looks fine :-/
After some tries, I luckily used a 'display: block;' style property and it starts to re-rendering absolutely fine. Also 'display: inline-block;' will fix that problem too, if it is needed to be used.
I had a similar problem where a center positioned span using flexbox inside a div wasn't updating correctly in Safari.
Restyling this div to use display:block solved this rendering issue so this might be worth looking into if anyone runs into the same problem in the future.
P.S. Looking at the styling code now, making the change was actually a much cleaner solution as it reduced the amount of lines from 4 to 1 but this might not be the case for all scenarios of course.
before:
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
after: (is a div so display:block is already used)
text-align: center;
When using map function , React looks at the keys :
To identify which items have changed, are added, or are removed.
Solution :
Now, As you use map function u must Add Key to the div inside map .
Use a key variable that is connected to the logic behind clicking , so that when clicking the key , the variable is updated and so React will rerender this grandchild div .
As the following code :
<div key={Count} className={'myComponent'}>{Count}</div>
PS : I discourage to use Date as a key , because I f u are clicking quickly it glitches , while if u are connecting it with a state with the logic behind clicking , it won't .
this may help you:
will-change: transform;
I know it may be late but what helped me with Safari not re-rendering reused components was:
will-change: opacity;
You may have to experiment which element in your HTML hierarchy to apply this to.
It makes no sense because no opacity was being changed in my case but it did fix the problem. Thank you, Apple...
We also ran into this problem. So, I did a short research about it and got the gist as follows:
It works by providing the unique key to the element(key={new Date()} works bcz unique even with seconds) which is not rerendering properly say a description as text inside it that changes by button or an event.
So what I got as a gist basically, providing the individual key make new dom element to react during reconcilation process rather than changing same value inside the element so basically that element showing object is made unique instead of changing value inside that dom element so I think safari specific it is as it is holding state of the element rendering dynamically. So after providing the key prop you are basically rendering the unique different element(obviously with different text/object inside it). This basically utilizes here the concept of keys while we render a list in react component.

React Side bar - to navigate similar to link provided

I am new to React JS and would like few ideas regarding implementing the side navigation.
We have Side Nav currently which gets generated from an API response in below structure.
A section may or may not have a subsection. Also, Subsection may or may not have a sub of sub-section. This can be infinite.
abc-SECTION
abcdef-SUB-SECTION
woirlew-SUB-SUB-SECTION
ABCDWER-SUB-SECTION
ABCDXDC
WERLLWWR-SUB-SUB-SECTION
xyz-SECTION
XYZSERF-SUB-SECTION
XYZlJIO-SUB-SECTION
owe-SECTION
abcdef-SUB-SECTION
ABCDWER-SUB-SECTION
slfjl-SUB-SECTION
We want to keep the tree structure as above but want to our tree structure to behave something similar to below link.
https://www.hindustantimes.com/interactives/aap-government-two-years-report-card/
We already have a side bar rendering implemented using recursive function. I am looking for scrolling behaviour.
Any guidance would be appreciated. What would be the good way to start with?
To create sidebar can create kind of recursive Component in reactjs.
class Tree extends Component {
render() {
return {
<div>
{this.props.children.map((child) => (
<div key={child.key}>
<span>{child.name}</span>
{child.children && <Tree children={child.children} />}
</div>
))}
</div>
}
}
}
In React to render a list you can have a separate function that takes a data structure as an input such as array or object and recursively calls itself to generate a UL, LI structure. Check this link out for recursively iterateing through an nested array of objects
For the scrolling behaviour you can usive javascript onClick event handler and window.scrollTop to calculate and scroll smoothly to that place alternatively theres a light-weight library for that behaviour Click here to Check it out

How to not re-render React component's specific DOM elements?

Recently I started to refactor my Backbone web app with React and I'm trying to write interactive graph visualization component using react and sigma.js.
I roughly understood React's declarative paradigm and how it is implemented by render() method using jsx syntax.
But what gets me stumbled is a situation where I cannot define my React component declarativly.
It is because of the javascript-generated DOM elements, which only can be generated on componentDidMount() after the declarative DOM elements are rendered by render().
It makes me worried about both performance and buggy animations (my graph animates on instantiation time, which will be re-played on every render() calls in this situation)
My current code looks like:
...
render: function() {
return (
<div class="my-graph-visualization-component">
{/*
This div is defined declaratively, so won't be re-rendered
on every `change` events* unless `React`'s diff algorithm
think it needs to be re-rendered.
*/}
<div class="my-declarative-div">{this.props.text}</div>
{/*
This div will be filled by javascript after the `render()`
call. So it will be always cleared and re-rendered on every
`change` events.
*/}
<div class="graph-container MY-PROBLEM-DIV"></div>
</div>
);
},
componentDidMount: function() {
this.props.sigmaInstance.render('.graph-container', this.props.graph);
}
...
Is there any way to do something like
render: function() {
return (
<div class="my-graph-visualization-component">
<div class="my-declarative-div">{this.props.text}</div>
{/*
Any nice workaround to tell react not to re-render specific
DOM elements?
*/}
<div class="graph-container NO-RE-RENDER"></div>
</div>
);
},
so that my sigma.js graph component won't get re-instantiated with identical starting animation on every change on states?
Since it seems to be it is about handling non-declarative part of react components, any workarounds for this kind of problem will be appreciated.
The cleanest way is to define react sub-components and re-render what you really need instead of re-rendering the whole block
render: function() {
return (
<div class='myStaticContainerNotupdated'>
<SubComponentToUpdateOften/>
<MyGraph/>
</div>
)
}
The other solution could be to work on your graph and implement a singleton so your animation is only played once at the first render.
But really the easiest and cleanest thing I see is to create clean separate subcomponent and update them when needed. You never update the big container component just the subs one.
Hope it helps
You can use dangerouslySetInnerHTML. This basically tells React to stay away from it’s content and it wont evaluate/update it when doing it’s DOM diffing.

Categories

Resources