I am having issues with a bug arising when I try to make a simple app where I can use the mousewheel to scroll up and down and each scroll of the mousewheel will navigate me to a new route.
One scroll down of the mousewheel should make the current route disappear from the screen in an upwards motion (in my code this is done by y: -1000), and the new route should appear starting from the bottom of the screen and moving upwards until it reaches y: 0.
For my first component (Main.js) this is easy enough as I can hardcode the initial and exit values of the animation because there are no routes about it, such as in the code below:
Main.js
import React, { useState, useRef, useEffect, useContext } from "react";
import { motion } from "framer-motion";
import { useLocation, useNavigate } from "react-router-dom";
export default function Main() {
const location = useLocation();
const navigate = useNavigate();
const { pathname } = location;
const [isUp, setIsUp] = useState(false);
useEffect(() => {
function handleNavigation(e) {
if (e.deltaY > 1) {
setIsUp(false);
navigate("/skills");
}
}
window.addEventListener("wheel", handleNavigation);
return () => window.removeEventListener("wheel", handleNavigation);
});
return (
<motion.div
className="main--container"
initial={{ y: -1000, transition: { duration: 0.8 } }}
animate={{ y: 0, transition: { duration: 0.8 } }}
exit={{ y: -1000, transition: { duration: 0.8 } }}
></motion.div>
);
}
The problem I'm having is I need the initial and exit values to change from either -1000 to 1000 or vice versa depending on if the user is mouse wheeling up or down.
my current structure for component Routes is as follows:
Main <-> Skills <-> AboutMe <-> Work
Where mousewheel down moves to the right -> and mousewheel up to the left <-
If I am on skills and I am going up to Main, I require the initial and exit values for the Skills component to be y:1000 for both, but if I am going down to the AboutMe component I need the Skills initial and exit values to be y: -1000, (and the AboutMe component would need to be y:1000 for both as well).
So my question is, How can I achieve dynamically changing these values in the most painless way possible?
I've tried the following:
passing down props from the AnimatedRoutes.js parent Component to try and update the value whilst also having it control which route to navigate to, but it appears on mounting / unmounting it doesn't give the correct props.
Having each component control its own state and and navigating to each component, this works for changing its own initial and exit values, but I also need it to work in tandem with the component its navigating to, I need both components to update their value before the transition occurs.
please take a look at a live example below, the first two components (Main.js & Skills.js) have hard coded values to show you how I intend for the transition to look like, if you mouse wheel up and down on these you will notice a smooth transition, however when moving down to the other components you will notice it is out of sync, sometimes it initialises / exits from the wrong direction, sometimes they go in opposite directions, it is quite an annoying problem that seems small but I have yet to be able to figure out.
Live Working Example:
https://stackblitz.com/edit/react-ts-w42djd?embed=1&file=App.tsx
Related
I have been struggling with this for days and am just having trouble wrapping my head around the varied documentation and examples out there, so I'm hoping someone can help with the specific example I'm working on and that might in turn help me understand this a bit better. This seems like it's far more convoluted than it should be, but I'm sure that's just my lack of understanding of how it all works.
The goal: make a very simple animation of a red ball moving back and forth. Hell, make any kind of animation at all (once I can confirm that gsap is animating anything at all, I should be able to take it from there).
The problem: Nothing happens. No errors, nothing clear to go on, just nothing. I still don't have a strong understanding of how this should work; a lot of the guides I've checked seem to be using different methods and don't go into a lot of detail as to why, so it's made it difficult to extend that knowledge to my specific scenario.
The code. I've simplified this greatly because, as I mentioned, all I really need is to just get any kind of gsap animation to work at all and from there I'm confident I can do with it what I need. If anyone feels like it would make a difference, I'm happy to update with the full code:
import React, { useRef, useEffect } from 'react';
import { gsap } from 'gsap';
const tl = gsap.timeline({paused: true, repeat: 0});
function App() {
const waitingAnimationRef = useRef(null);
useEffect(() => {
tl.set(waitingAnimationRef, {autoAlpha: 0});
tl.play();
}, []);
return (
<div className="App">
<div id="red-circle" ref={waitingAnimationRef}></div>
</div>
);
}
export default App;
Here's a working example and some tips to help you make some sense of it:
Create a timeline with gasp.timeline and store it in a ref. You can do this as you've done, creating the timeline outside your component, but then you need to pass that timeline to the ref in your component. In this example, I did that by passing the variable name for the timeline to the useRef hook directly: const tl = useRef(timeline);.
I used the timeline options { repeat: -1, yoyo: true } so that the animation would loop infinitely in alternating directions because you said you wanted to make a ball "moving back and forth".
You'll also need a DOM node ref so you can pass that to the gsap.context() method. Create the ref in your component and pass it to the wrapping element of the component. Here, I called mine app (const app = useRef(null)) and then passed it to the top level div in the App component with ref={app}. Make sure you're passing the ref to a DOM node and not a React component (or you'll have to forward the ref down to a node child within it). Why are we using refs? Because refs are stable between rerenders of your components like state, but unlike state, modifying refs don't cause rerenders. The useRef hook returns an object with a single property called current. Whatever you put in the ref is accessed via the current property.
Use a useLayoutEffect() hook instead of useEffect. React guarantees that the code inside useLayoutEffect and any state updates scheduled inside it will be processed before the browser repaints the screen. The useEffect hook doesn't prevent the browser from repainting. Most of the time we want this to be the case so our app doesn't slow-down. However, in this case the useLayoutEffect hook ensures that React has performed all DOM mutations, and the elements are accessible to gsap to animate them.
Inside the useLayoutEffect, is where you'll use the gsap context method. The gsap context method takes two arguments: a callback function and a reference to the component. The callback is where you can access your timeline (don't forget to access via the current property of the ref object) and run your animations.
There are two ways to target the elements that you're going to animate on your timeline: either use a ref to store the DOM node or via a selector. I used a selector with the ".box" class for the box element. This is easy and it's nice because it will only select matching elements which are children of the current component. I used a ref for the circle component. I included this as an example, so you could see how to use forwardRefs to pass the ref from the App component through Circle component to the child div DOM node. Even though this is a more "React-like" approach, it's harder and less flexible if you have a lot of elements to animate.
Just like useEffect, useLayoutEffect returns a clean up function. Conveniently, the gsap context object has a clean up method called revert.
import { useLayoutEffect, useRef } from "react";
import gsap from "gsap";
const timeline = gsap.timeline({ repeat: -1, yoyo: true });
function App() {
const tl = useRef(timeline);
const app = useRef(null);
const circle = useRef(null);
useLayoutEffect(() => {
const ctx = gsap.context(() => {
tl.current
// use scoped selectors
// i.e., selects matching children only
.to(".box", {
rotation: 360,
borderRadius: 0,
x: 100,
y: 100,
scale: 1.5,
duration: 1
})
// or refs
.to(circle.current, {
rotation: 360,
borderRadius: 50,
x: -100,
y: -100,
scale: 1.5,
duration: 1
});
}, app.current);
return () => ctx.revert();
}, []);
return (
<div ref={app} className="App">
<Box />
<Circle ref={circle} />
</div>
);
}
Context
I like to use pixi.js for a game I have in mind. Over the years I always used React and next.js.
So what I want to do it use pixi.js inside React / Next.
There are 2 packages out there that integrate into react fiber to render component like but they are outdated and give so many errors to me (for example doesn't work with react 18 and also have poor documentations). (#inlet/react-pixi and react-pixi-fiber)
So I decided to go the useRef route.
Here is some code that works fine:
import { useRef, useEffect } from "react";
import { Application, Sprite, Texture } from "pixi.js";
import bunnyImg from "../public/negx.jpg";
const app = new Application({
width: 800,
height: 600,
backgroundColor: 0x5bba6f,
});
const Pixi = () => {
const ref = useRef();
useEffect(() => {
// On first render add app to DOM
ref.current.appendChild(app.view);
// Start the PixiJS app
app.start();
const texture = Texture.from(bunnyImg.src);
const bunny = new Sprite(texture);
bunny.anchor.set(0.5);
bunny.x = 0;
bunny.y = 0;
bunny.width = 100;
bunny.height = 100;
app.stage.addChild(bunny);
return () => {
// On unload stop the application
app.stop();
};
}, []);
return <div ref={ref} />;
};
export default Pixi;
The Problem
The only problem I have with this is, that hot reload doesn't work. So if I change something in the useEffect hook, I have to go into the browser and manually refresh the page. So basically hot reloading doesn't work.
I think since it uses a ref that basically never changes.
The question
Is there a better way of coding pixi.js inside react / next?
It's probably unrelated to react-pixi. It looks like the default behavior of hot reloading as described on the Github repo.
Hooks would be auto updated on HMR if they should be. There is only one condition for it - a non zero dependencies list.
and
❄️ useEffect(effect, []); // "on mount" hook. "Not changing the past"
The code in the question is using such an empty dependency list, so the behavior is as expected.
Please post the full setup including the hot reload configuration if this is not the issue.
If you want Pixi/Canvas gets auto refreshed on the rendering cycle, you probably need to define those values with react states and add those states to the useEffect dependencies
For example:
const [bunnyProps, setBunnyProps] = React.useState({
anchor: 0.5,
x: 0,
y: 0,
width: 100,
height: 100,
})
React.useEffect(() => {
// ...
bunny.x = bunnyProps.x;
bunny.y = bunnyProps.y;
// ...
}, [bunnyProps])
I've got a sample Resium project that shows the map. I've added a simple onClick that sets some state which is NOT being used anywhere - I just set it. Still, it causes the entire map to redraw & flicker UNLESS I remove the terrainProvider. Example (terrainProvider is commented out) if you move/click the mouse, the entire Cesium UI flickers and redraws everything. I'm using React 17, Resium 1.14.3 (Cesium 1.86.1). Any idea what's going on?
import React, { useState } from "react";
import { Cartesian3, Color } from "cesium";
import { Viewer, Entity } from "resium";
import * as Cesium from 'cesium';
export default function App() {
const [currentPos, setCurrentPos] = useState(null);
const handleMouseClick = (e, t) => {
setCurrentPos("xxx");
}
return (
<Viewer full
onClick={handleMouseClick}
onMouseMove={handleMouseClick}
// terrainProvider={new Cesium.CesiumTerrainProvider({ url: 'https://api.maptiler.com/tiles/terrain-quantized-mesh-v2/?key=xxxxxxx' })}
>
<Entity
name="Tokyo"
position={Cartesian3.fromDegrees(139.767052, 35.681167, 100)}
point={{ pixelSize: 10, color: Color.RED }}
/>
</Viewer>
);
}
My guess is this. No matters that the currentPos is not used in the component, is still part of the component's state, then the components re-renders.
In every render, the terrainProvider prop is receiving a new instance, so this causes that the entire Viewer re-renders too.
Maybe you can save your instance as state in the component and don't create a new one everytime that the component is re-render.
I thought react-spring useSpring() causes the component to re-render a lot, so if it is a component that already has a lot of CPU intensively work to do, then react-spring is not best suited for the task.
I can see that it re-renders a lot, in their example:
https://codesandbox.io/s/musing-dew-9tfi9?file=/src/App.tsx
(by looking at the console.log output, which has a lot of print out of renderCount. The print out is a lot more when we change the duration to 5000 which is 5 seconds).
Likewise, if it is a component that is similar to react-spring, it'd render a lot:
https://codesandbox.io/s/wonderful-currying-9pheq
However, the following code:
let renderCount = 0
export default function App() {
const styles = useSpring({
loop: true,
to: [
{ opacity: 1, color: '#ffaaee' },
{ opacity: 0.5, color: 'rgb(14,26,19)' },
{ transform: 'translateX(100px)' },
{ transform: 'translateX(0px)' },
],
from: { opacity: 0, color: 'red', transform: 'translateX(0px)' },
config: { duration: 500 },
})
console.log('renderCount', ++renderCount)
return <a.div style={styles}>I will fade in and out</a.div>
}
Demo: https://codesandbox.io/s/dazzling-rgb-j2bx3?file=/src/App.tsx
We can see that the renderCount hardly get printed out at all. react-spring should need to keep on updating the style of the component, so after a minute, I'd expect a lot of print out of renderCount like the first two examples above, but it does not.
How and why doesn't react-spring cause a lot of re-rendering in this case, and how do we know in what situation would react-spring cause a lot of re-rendering (and how to prevent it)?
react-spring updates styles incrementally to create animations (as opposed to css animations with transition).
Naive animations outside React
If react-spring was to exist outside of React (which it OBVIOUSLY doesn't because then it wouldn't be named react-spring), this could most easily be done by modifying a given element's style by means of Javascript according to some predetermined pattern based on multiple factors (like delay, duration, etc....). One scenario could be
...
setTimeout(() => document.getElementById("#el").style.opacity = 0.34,100)
setTimeout(() => document.getElementById("#el").style.opacity = 0.39,150)
setTimeout(() => document.getElementById("#el").style.opacity = 0.42,200)
...
setTimeout(() => document.getElementById("#el").style.opacity = 1.0, 1000)
Exactly how this would be implemented is of course not the point of this answer and the above would be a very naive implementation, but this is basically what could go on if we wanted to make some animated transition where the interpolation between two endpoints would be calculated and implemented by ourselves (using spring physics) as opposed to in the browser (with css transition).
Naive animations in React
In React, we know that the preferred way to do things is to provide changes inside React, which React then processes after which necessary changes to the DOM is handled by React. Taking the previous (naive) example to React, this would imply some scheme where a state storing the opacity would be updated repeatedly until the desired endpoint was reached.
const Component = () => {
...
const [opacity, setOpacity] = useState(0)
useEffect(() => {
...
setTimeout(() => setOpacity(0.34),100)
setTimeout(() => setOpacity(0.39),150)
setTimeout(() => setOpacity(0.42),200)
...
setTimeout(() => setOpacity(1.0), 1000)
}, [])
return (
<div style={{ opacity }} ... />
)
}
This would work, but as one would expect, it could be quite burdensome since animations are supposed to happen fast and smooth and React rerendering on every animation frame could be problematic; if the component within which animation took place was expensive to render, the animation itself could be suffering and not look very good.
react-spring in React
The solution to this problem by react-spring is to do updates OUTSIDE of React via refs instead. The previous toy example could look like:
const Component = () => {
...
const ref = useRef(null)
useEffect(() => {
if(ref.current) {
...
setTimeout(() => ref.current.style.opacity = 0.34,100)
setTimeout(() => ref.current.style.opacity = 0.39,150)
setTimeout(() => ref.current.style.opacity = 0.42,200)
...
setTimeout(() => ref.current.style.opacity = 1.0, 1000)
}
}, [])
...
return (
<div ref={ref} ... />
)
}
Again, this is an example, exactly how one would implement this in the best way (as in react-spring) is a different story. But we can agree on that if we would log to the console every time the above component rendered, it would only log once even though the opacity would continue to change.
To sum up, when react-spring is used optimally, it uses refs to update properties on DOM elements whereupon React is by-passed. Thanks to this, a component may render only once but still make repeated animations. This particularly applies to the situation when the api is used to perform updates (as oppose to storing a state in a parent component which is set every time we want an animation to take place):
const [spring, api] = useSpring(() => ({ <INITIAL PROPS> })) // use api to make changes
const spring = useSpring({ <INITIAL PROPS }) // rerender component to update props
When using the basic HTML elements supplied by react-spring (such as animated.div, animated.span etc...), react-spring takes care of attaching a ref on the corresponding DOM element and via this ref, it manages to animate the element and therefore also all the content in it. When creating your own custom component wrapped in animated, it is your concern to make sure that your custom component can take a ref (via forwardRef) and to pass it on to the element which should be animated, if you want optimal animations. If you don't do this, the element will be rerendered on every animation frame by react-spring. Even though this works too, it is suboptimal from a performance point of view.
Your examples
In your examples some other things are at play as well. In the first example, the hook useMeasure is being used from react-use-measure. This hook will continuously provide different values from the child component (here height is provided) whereupon the parent will rerender. Since Tree components are nested, whenever ONE Tree component changes height, all parent Tree components whose heights will be changed will also rerender. We therefore see quite a lot of rerendering. Also, because StrictMode is enabled, the number is doubled. useSpring is used without api but it doesn't matter here since the parent rerenders a lot due to useMeasure anyways.
In your second example with react-spring, the api is not used either but since the animation is looped, it doesn't require any state in the parent to be set and so it doesn't rerender. Because the parent doesn't rerender, the animated component doesn't rerender either and so also in this case, it doesn't matter if we use the api or not. In this example if we would like to update the animated props however, using the api to do so would cause the parent NOT to rerender whereas otherwise, the state holding the props to update to would reside in the parent (or a grand-parent) and so the animated component would rerender as well when the parent rerenders.
I'd like to be able to display text feedback to a user after they answer a question and then fade out the text each time they answer. I'm trying a very simple situation to start out with where I play static feedback after every question. The text between the div is displayed and then fades after the initial rendering of the component but on subsequent renders the text is not displayed and the animation does not occur (I'm using Chrome). I can confirm the component is being re-rendered with Chrome Dev Tools after each cycle and I can see the text in the DOM. I'm using forwards so that at the end of the animation the text will stay invisible. The problem I'm trying to solve is why does the animation only occur after the first render cycle and what do I need to do in order to animate each time the component renders? Other details are that the app uses Redux and all components are functional.
Here's a sandbox that shows what the issue looks like. In this case I passed in props to the feedback component to force it to re-render. Each time you type into the text input it forces the feedback component to re-render (which I confirmed by logging to the console) but the animation only plays on the first render.
https://codesandbox.io/s/reactcssanimationissue-zwc6l?file=/src/UserFeedBack/UserFeedBackComponent.js
import React from "react";
import classes from "./AnswerFeedBackComponent.module.css";
const AnswerFeedBackComponent = () => {
return (
<div className={classes.CorrectAnswer} >Correct!</div>
);
}
export default AnswerFeedBackComponent;
---
.CorrectAnswer{
color:green;
font-weight: bold;
animation: fade 3s linear forwards;
opacity: 1;
}
#keyframes fade {
0% {
opacity: 1;
}
100%{
opacity: 0;
}
}
During re-render react looks for only the elements which have changed. If not, then it doesn't update the element, even if it rerenders.
So you can either make react know that this element have changed, or unmount and mount the component again. I have provided both the usecases.
You can add a key prop with a random value, so that react knows its different on each re-render and hence remounts the component. This is quick and hacky and it works.
const UserFeedBackComponent = (props) => {
console.log("UserFeedBackComponent rendered");
const rand = Math.random();
return (
<div className={classes.Border} key={rand}>
<div className={classes.CorrectAnswer}>Correct!</div>
</div>
);
};
https://codesandbox.io/s/reactcssanimationissue-forked-9jufd
Another way would be to remove it from the dom after its invisible. The logic will be handled by its parent, InputComponent
import React, { useState } from "react";
import UserFeedBackComponent from "../UserFeedBack/UserFeedBackComponent";
import classes from "./InputComponent.module.css";
const InputComponent = () => {
const [currentInput, setCurrentInput] = useState("");
const [showNotification, setShowNotification] = useState(false);
const inputHandler = (event) => {
setCurrentInput(event.target.value);
setShowNotification(true);
setTimeout(() => {
setShowNotification(false);
}, 3000);
};
return (
<React.Fragment>
<input type="text" value={currentInput} onChange={inputHandler} />
<div className={classes.Output}>{"Output is " + currentInput}</div>
{showNotification && <UserFeedBackComponent text={currentInput} />}
</React.Fragment>
);
};
export default InputComponent;
Every time you need to show the notification, you just need to set showNotification to true, and set it to false again via setTimeout which will fire after the duration of your animation i.e 3s. This is much better because, there's no stale invisible element, polluting your dom. You can still iterate and improve upon it.
https://codesandbox.io/s/reactcssanimationissue-forked-mejre