How to execute two animations sequentially, using react-spring? - reactjs

I tried chaining two springs (using useChain), so that one only starts after the other finishes, but they are being animated at the same time. What am I doing wrong?
import React, { useRef, useState } from 'react'
import { render } from 'react-dom'
import { useSpring, animated, useChain } from 'react-spring'
function App() {
const [counter, setCounter] = useState(0)
const topRef = useRef()
const leftRef = useRef()
const { top } = useSpring({ top: (window.innerHeight * counter) / 10, ref: topRef })
const { left } = useSpring({ left: (window.innerWidth * counter) / 10, ref: leftRef })
useChain([topRef, leftRef])
return (
<div id="main" onClick={() => setCounter((counter + 1) % 10)}>
Click me!
<animated.div id="movingDiv" style={{ top, left }} />
</div>
)
}
render(<App />, document.getElementById('root'))
Here's a codesandbox demonstrating the problem:
https://codesandbox.io/s/react-spring-usespring-hook-m4w4t

I just found out that there's a much simpler solution, using only useSpring:
function App() {
const [counter, setCounter] = useState(0)
const style = useSpring({
to: [
{ top: (window.innerHeight * counter) / 5 },
{ left: (window.innerWidth * counter) / 5 }
]
})
return (
<div id="main" onClick={() => setCounter((counter + 1) % 5)}>
Click me!
<animated.div id="movingDiv" style={style} />
</div>
)
}
Example: https://codesandbox.io/s/react-spring-chained-animations-8ibpi

I did some digging as this was puzzling me as well and came across this spectrum chat.
I'm not sure I totally understand what is going on but it seems the current value of the refs in your code is only read once, and so when the component mounts, the chain is completed instantly and never reset. Your code does work if you put in hardcoded values for the two springs and then control them with turnaries but obviously you are looking for a dynamic solution.
I've tested this and it seems to do the job:
const topCurrent = !topRef.current ? topRef : {current: topRef.current};
const leftCurrent = !leftRef.current ? leftRef : {current: leftRef.current};
useChain([topCurrent, leftCurrent]);
It forces the chain to reference the current value of the ref each time. The turnary is in there because the value of the ref on mount is undefined - there may be a more elegant way to account for this.

Related

Why lottieRef returns only null?

Using the library "# lottiefiles/react-lottie-player"
You need to get lottieRef to interact with animation, but I get null.
Code reference: https://codesandbox.io/s/great-rgb-7dp4j0?file=/src/HorizontalPicker/HorizontalPicker.jsx
import React, {useEffect, useRef} from "react";
import {Player} from "#lottiefiles/react-lottie-player";
export default function App() {
const player = useRef(null)
const lottie = useRef(null)
useEffect(() => {
if(lottie && lottie.current){
console.log(lottie.current) //return null
}
}, [])
return (
<div className="App">
<Player
lottieRef={data => lottie.current = data}
ref={player}
onEvent={event =>{
if(event === "load"){
lottie.current.play() //nothing
}
}}
keepLastFrame={true}
autoplay={false}
loop={true}
src={"https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json"}
style={{width: "100%", height: "2.5em", padding: "0", margin: "0"}}/>
</div>
);
}
There is also an interesting point.
If you output lottie, we get an object with null (while there is something inside it)
And if you output lottie.current, we get null.
Reference to an example of this thing: https://ibb.co/RQWxLkJ
Can you pass the lottie ref into your ref like the following (see ref on the div):
export default function App() {
const player = useRef(null)
const lottie = useRef(null)
useEffect(() => {
if(lottie && lottie.current){
console.log(lottie.current) //return null
}
}, [])
return (
<div className="App">
<Player
lottieRef={data => lottie.current = data}
ref={player}
onEvent={event =>{
if(event === "load"){
lottie.current && lottie.current.play() //nothing
}
}}
keepLastFrame={true}
autoplay={false}
loop={true}
src={"https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json"}
style={{width: "100%", height: "2.5em", padding: "0", margin: "0"}}/>
</div>
);
}
This way I am able to get the animation object when I console lottie.current
I'd prefer to use useState to save the animationData rather than a ref. But there was also an issue on the player where the 'load' event was firing but the player hadn't finish setting its internal instance of the animation therefor calling play() wouldn't work. This was happening only on functional components in React that's why it went undiscovered.
I've made a few changes to your code and to the player, using v1.5.2 you should be able to accomplish what you're looking for:
import css from "./HorizontalPicker.module.css";
import React, { useRef, useState, useEffect } from "react";
import { Player } from "#lottiefiles/react-lottie-player";
const HorizontalPicker = () => {
const player = useRef(null);
// const lottie = useRef(null);
const [lottie, setLottie] = useState(null);
useEffect(() => {
if (lottie) {
console.log(" Lottie animation data : ");
console.log(lottie);
// You can also play by calling the underlying lottie method
// lottie.play();
}
}, [lottie]);
return (
<>
<Player
onEvent={(event) => {
// console.log(event);
if (event === "instanceSaved" && player && player.current) {
console.log("Playing animation..");
player.current.play();
}
}}
lottieRef={(data) => {
setLottie(data);
}}
ref={player}
keepLastFrame={true}
autoplay={false}
loop={true}
src={
"https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json"
}
/>
</>
);
};
export default HorizontalPicker;
Sandbox:
https://codesandbox.io/s/lottie-react-functional-component-2bs8fs?file=/src/HorizontalPicker/HorizontalPicker.jsx
Cheers!
Well, according to official documentation https://github.com/LottieFiles/lottie-react, lottieRef represents a callback function which is fired by Player component (and this function returns AnimationFrame object)
I'm not familiar with this library, and whatever I'll describe next are just my assumptions :) Seems that whenever player "plays", it displays frames one-by-one (from json file in "src" attribute"). And whenever player displays frames from .json file - Player fires "lottieRef" event which you utilize to set lottie.current. And player starts displaying frames only when it loads .json data using "src" parameter in Player definition (see network tab in your browser to ensure that additional http request presents)
And in this case everything seems pretty logical: you try to access "lottie" variable in useEffect of your component but it's empty as far as Player did not manage to display any frame yet because the callback (lottieRef) did not fire yet as far as .json file is not loaded yet. No matter if .json file is large or small, Player requests it via additional http call, which requires some (even minital) amount time. And useEffect fires before .json is loaded immediately after rendering DOM (that's how ReactJS works)
On the other hand, if you delay a bit request to "lottie" ref - you'll see that it is populated:
Code example:
import React, { useEffect, useRef } from 'react';
import { Player } from '#lottiefiles/react-lottie-player';
export default function App() {
const player = useRef(null);
const lottie = useRef(null);
useEffect(() => {
setTimeout(() => console.log(lottie), 50);
}, []);
return (
<div className="App">
<Player
lottieRef={(data) => (lottie.current = data)}
ref={player}
keepLastFrame={true}
autoplay={false}
loop={true}
src={
'https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json'
}
/>
</div>
);
}
So if you delay onEvent callback event a bit, "lottie" ref would be initialized by that moment and .play() would work:
onEvent={(event) => {
event === 'load' &&
setTimeout(
() => lottie.current && lottie.current.play()
);
}
}
BUT, if the only purpose you have is to execute Player whenever it's ready - why not to use "autoplay" built-in property ? (autoplay={true})
import React, { useRef } from 'react';
import { Player } from '#lottiefiles/react-lottie-player';
export default function App() {
const player = useRef(null);
const lottie = useRef(null);
return (
<div className="App">
<Player
lottieRef={(data) => (lottie.current = data)}
ref={player}
keepLastFrame={true}
autoplay={true}
loop={true}
src={
'https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json'
}
/>
</div>
);
}
Hope it'll help

React Slider images changing on Click not working (uncaught error too many re-renders)

I'm trying to create a React slider for images. But I'm getting an error that says Uncaught Error: Too many re-renders. If anyone can just point me in the right direction I would really appreciate it. I'm certain that the issue lays within the onClick aspect of the sliderDots mapping.
import React, { useEffect, useState } from 'react';
import Sliderdots from '../CarasouelDots/Sliderdots.component';
import './Slider.styles.scss'
import sliderImages from '../../MockImages/mockimages';
const Slider = () => {
const images = sliderImages;
//Iterator
const [img, setImg] = useState(0);
//Getting all shoe images from an object array
const shoes = images.map(i => (i.shoe));
const heading = images.map(i => (i.title));
const content = images.map(i => (i.content))
const numbers = shoes.map((i, index) => (index))
const indexSet = (number) =>{
setImg(number);
}
//problem with onClick here??
const sliderD = images.map((dot, index) => <Sliderdots key={index} onClick={indexSet(index)}/>);
useEffect(() => {
const timer = setTimeout(() => {
img == shoes.length - 1 ? setImg(0) : setImg(img + 1)
}, 4500)
}, [img]);
return (
<div className='slider-container' style={{ backgroundImage: `url(${shoes[img]})` }}>
<div className='overlay'>
<h1 className='introduction'>{heading[img]}</h1>
<p className='content'>{content[img]}</p>
<div className='dot-container'>
{sliderD}
</div>
</div>
</div>
);
};
export default Slider;
The reason why your component is constantly rerendering is because your onClick property is actually a function call in disguise that gets executed every render:
// This line actually calls the `indexSet` function each time!
const sliderD = images.map((dot, index) => <Sliderdots key={index} onClick={indexSet(index)}/>);
And since indexSet updates the state of the React component by calling setImg, the React component will always end up re-rendering when it reaches that line of code, and since that line of code always re-calls the indexSet function, your component will infinitely re-render.
To fix your code, you just need to replace that onClick property with an anonymous function:
const sliderD = images.map((dot, index) => <Sliderdots key={index} onClick={() => indexSet(index)}/>);

Applying state change to specific index of an array in React

Yo there! Back at it again with a noob question!
So I'm fetching data from an API to render a quizz app and I'm struggling with a simple(I think) function :
I have an array containing 4 answers. This array renders 4 divs (so my answers can each have an individual div). I'd like to change the color of the clicked div so I can verify if the clicked div is the good answer later on.
Problem is when I click, the whole array of answers (the 4 divs) are all changing color.
How can I achieve that?
I've done something like that to the divs I'm rendering :
const [on, setOn] = React.useState(false);
function toggle() {
setOn((prevOn) => !prevOn);
}
const styles = {
backgroundColor: on ? "#D6DBF5" : "transparent",
};
I'll provide the whole code of the component and the API link I'm using at the end of the post so if needed you can see how I render the whole thing.
Maybe it's cause the API lacks an "on" value for its objects? I've tried to assign a boolean value to each of the items but I couldn't seem to make it work.
Thanks in advance for your help!
The whole component :
import React from "react";
import { useRef } from "react";
export default function Quizz(props) {
const [on, setOn] = React.useState(false);
function toggle() {
setOn((prevOn) => !prevOn);
}
const styles = {
backgroundColor: on ? "#D6DBF5" : "transparent",
};
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
let temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}
let answers = props.incorrect_answers;
const ref = useRef(false);
if (!ref.current) {
answers.push(props.correct_answer);
shuffleArray(answers);
ref.current = true;
}
const cards = answers.map((answer, key) => (
<div key={key} className="individuals" onClick={toggle} style={styles}>
{answer}
</div>
));
console.log(answers);
console.log(props.correct_answer);
return (
<div className="questions">
<div>
<h2>{props.question}</h2>
</div>
<div className="individuals__container">{cards}</div>
<hr />
</div>
);
}
The API link I'm using : "https://opentdb.com/api.php?amount=5&category=27&type=multiple"
Since your answers are unique in every quizz, you can use them as id, and instead of keeping a boolean value in the state, you can keep the selected answer in the state, and when you want render your JSX you can check the state is the same as current answer or not, if yes then you can change it's background like this:
function Quizz(props) {
const [activeAnswer, setActiveAnswer] = React.useState('');
function toggle(answer) {
setActiveAnswer(answer);
}
...
const cards = answers.map((answer, key) => (
<div key={key}
className="individuals"
onClick={()=> toggle(answer)}
style={{background: answer == activeAnswer ? "#D6DBF5" : "transparent" }}>
{answer}
</div>
));
...
}

Order of useCallback declaration

Does the order of useCallback function declaration matters? For example:
Functions declared in this order:
const aboveFunction = useCallback(()=>{ logCount() },[logCount])
const logCount = useCallback(()=>{ console.log(count) },[count])
const belowFunction = useCallback(()=>{ logCount() },[logCount])
Notice both aboveFunction and belowFunction refers to the same logCount.
After we call setCount(2) ,
aboveFunction() -> logCount() -> count // 0, wheres
belowFunction() -> logCount() -> count // 2
https://codesandbox.io/s/react-hooks-counter-demo-forked-7mofy?file=/src/index.js
/**
Click increment button until count is 2, then click
Above and Below button and check console log
* Even though the count value is > 0, aboveIncrement console.log(count) still gives 0
*/
import React, { useState, useCallback } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [count, setCount] = useState(0);
/** Assuming all the functions below do some intensive stuffs to justify useCallback */
const aboveFunction = useCallback(() => {
// ...do other intensive stuff
logCount("Above count:"); // Above count: 0
}, [logCount]);
const logCount = useCallback(
(str) => {
// ...do other intensive stuff
console.log(str, count);
},
[count]
);
const belowFunction = useCallback(() => {
// ...do other intensive stuff
logCount("Below count:"); // Above count: 2
}, [logCount]);
return (
<div className="App">
<h1>
Click increment button until count is 2, then click Above/ Below button
and see console log
</h1>
<h2>You clicked {count} times!</h2>
<div
style={{
display: "flex",
alignItems: "center",
flexDirection: "column"
}}
>
<button style={{ margin: 10 }} onClick={aboveFunction}>
Above Increment Function
</button>
<button style={{ margin: 10 }} onClick={() => setCount(count + 1)}>
Increment
</button>
<button style={{ margin: 10 }} onClick={belowFunction}>
Below Increment Function
</button>
</div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
It is apparent that the order of useCallback declaration is important in order for the code to work. However, if we write it this way
https://codesandbox.io/s/react-hooks-counter-demo-forked-7mofy?file=/src/index1.js
let logCount;
const aboveFunction = useCallback(() => {
logCount("Above count:"); // Above count: 0
}, [logCount]);
logCount = useCallback((str) => {
console.log(str, count);
}, [count]
);
const belowFunction = useCallback(() => {
logCount("Below count:"); // Above count: 2
}, [logCount]);
aboveFunction still gives stale count value. Is there any rule-of-hooks that I'm missing?
Yes a fix here is to apply eslint no-use-before-define. But I wanna understand the reason why .. isn’t functions get hoisted up in JavaScript and the order in which it is declared doesn’t matter?
Is there any rule-of-hooks that I'm missing?
You are not missing rule-of-hooks but, you are missing no-use-before-define rule.
You have to use the logCount function above the aboveFunction.
In this attached image as well
you can see, it's showing a warning for function declaration.
Maybe you can add es-lint in your project and make sure you add this plugin & no-use-before-define as well so that it will show you these types of warning in vs code.
Hope this helps!

How to re-animate react-spring animation using hooks on button click?

Following simple component from the official examples:
import {useSpring, animated} from 'react-spring'
function App() {
const props = useSpring({opacity: 1, from: {opacity: 0}})
return <animated.div style={props}>I will fade in</animated.div>
}
Question
How do I animate the fadeIn-effect (or any other animation) again for example when I click on a button or when a promise is resolved?
You can basically make two effect with useSpring and an event.
You can change the style for example the opacity depending on the state of an event.
You can restart an animation on state change. The easiest way to restart is to rerender it.
I created an example. I think you want the second case. In my example I rerender the second component with changing its key property.
const Text1 = ({ on }) => {
const props = useSpring({ opacity: on ? 1 : 0, from: { opacity: 0 } });
return <animated.div style={props}>I will fade on and off</animated.div>;
};
const Text2 = () => {
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
return <animated.div style={props}>I will restart animation</animated.div>;
};
function App() {
const [on, set] = React.useState(true);
return (
<div className="App">
<Text1 on={on} />
<Text2 key={on} />
<button onClick={() => set(!on)}>{on ? "On" : "Off"}</button>
</div>
);
}
Here is the working example: https://codesandbox.io/s/upbeat-kilby-ez7jy
I hope this is what you meant.

Resources