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>
);
}
Related
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
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.
Not so fluent with React hooks, used plenty of class components before, hope you'll be forgiving.
The current code causes infinite re-rendering, and I think I understand why - the entire function body is being called on re-render.
const NavTabs = () => {
const classes = useStyles();
const [categories, setCategories] = React.useState();
const axiosPromise = getRequest(consts.categoriesURL);
axiosPromise.then(data => {
setCategories(data.value);
})
return (
<div className={classes.root}>
<AppBar position="static">
</AppBar>
{categories && <DynamicTabs categories={categories}/>}
</div>
);
}
I guess I could do something like if (!categories) { const axiosPromise [...] and so forth, i.e. do the http request only if categories haven't been populated yet. I guess this could also be solved by useEffect? Or wrapping the hook in an internal function?
I guess my real question is - why is React re-rendering the entire function body? Shouldn't it re-render only the return function? And then what is the point of using hooks that will be re-run on every render?
Compared to class components - shouldn't the code in the function body be equivalent to the constructor code in class components, and the return function - equivalent to the render method?
I guess I could do something like if (!categories) { const axiosPromise [...] and so forth, i.e. do the http request only if categories haven't been populated yet. I guess this could also be solved by useEffect? Or wrapping the hook in an internal function?
Yes, useEffect is the way to go here. Making a request and setting the result as state are side effects should only be run once in your case. We can achieve that easily with useEffect.
I guess my real question is - why is React re-rendering the entire function body? Shouldn't it re-render only the return function? And then what is the point of using hooks that will be re-run on every render?
React has no way to split a js function and only re-render the return. The function is atomic and must be completed. That is what hooks are for. React controls when hooks are ran so it can do fun stuff like batch state updates, ignore outdated effects and prioritise high priority work like animations.
Compared to class components - shouldn't the code in the function body be equivalent to the constructor code in class components, and the return function - equivalent to the render method?
The functional component is equivalent to the render method of a class component. They are called in a similar way. All the other lifecycle methods are replaced by hooks.
I recommend the react docs are great place to start and Dan Abramov has a great deep dive on hooks.
Yes, getRequest is being invoked each render cycle which sets some state and triggers a rerender. Placing it in an effect hook with a dependency array is likely the best solution. What dependencies you define will dictate when getRequest can be invoked.
Why is React re-rendering the entire function body?
The entire function body needs to run in order to determine the return value.
And then what is the point of using hooks that will be re-run on every render?
Hooks are run on every render, in the same order they are defined, but depending on dependencies may not invoke a callback. Hooks are what give functional components so much viability and sense of component lifecycle, to nearly be equivalent to class-based components in functionality. In most cases, you can completely convert a class-based component to a functional one and not drop any functionality.
Compared to class components - shouldn't the code in the function body be equivalent to the constructor code in class components, and the return function - equivalent to the render method?
It is more accurate to think of the entire functional components definition as the class-based render function, which can contain some logic and returns computed JSX to render to the DOM.
Example Solution:
const NavTabs = () => {
const classes = useStyles();
const [categories, setCategories] = React.useState(); // <-- no initial state!
useEffect(() => {
getRequest(consts.categoriesURL).then(data => {
setCategories(data.value); // <-- will update state and trigger render
});
}, []); // <-- empty dependency is run once on component mount
return (
<div className={classes.root}>
<AppBar position="static">
</AppBar>
{categories && <DynamicTabs categories={categories}/>}
</div>
);
}
To answer "why react is running the entire function" the answer is that javascript functions work that way: you always have to run the whole thing, they don't stop in the middle*. I understand what you are thinking here, if you are used to class components: don't I have a constructor section and a render section? and the answer is: not really if you are using function components. You only have render. But hooks are magic, and they let you pretend to have two parts.
Hooks know when they are called, and assuming you always call them in the same order, the can keep track of state outside the render function. so the way the work is sorta like this:
React detects a function component and creates or re-uses an existing rendering context for that component. This is where the hook information lives.
React calls your function component and it starts running.
You call hooks within your function component. These check what the current rendering context is, and save/get relevant information from that context. In a sense the rendering context is a "global" variable.
You do whatever else you want within the function, and eventually return a component tree (JSX) or null.
react then (eventually) updates the DOM to match what you returned, and saves the changes to the rendering context, so the next time render is called, it can re-use the context.
The magic is that the rendering context can do fancy things with hooks, like only run them once, always return the same value from a hook, or any other number of things. But in a sense, the component "class" becomes the react-internal rendering context that hooks know how to access.
Here is an example of the useState hook implemented in a class component: (You wouldn't ever need to do this, but it's an example of how hooks work).
class FakeHook extends React.Component {
constructor(...args) {
super(...args)
this.state = {}
this.useStateCalls = 0
}
useState(defaultValue){
const currentRenderContext = this.state
let value = defaultValue
const currentStateKey = `useState${this.useStateCalls}`
if (currentStateKey in currentRenderContext) value = currentRenderContext[currentStateKey]
this.useStateCalls++
return[value, (newValue) => this.setState({[currentStateKey]: newValue})]
}
render(){
this.useStateCalls = 0
let [fooState, setFoo] = this.useState("foo default")
let [barState, setBar] = this.useState("bar default")
return(
<dl>
<dt>Foo state</dt>
<dd>
<strong>Value:</strong>
<div>{fooState}</div>
<button onClick={(event) => {event.preventDefault(); setFoo(`foo updated at ${new Date().toLocaleString()}`)}}>Update Foo</button>
</dd>
<dt>Bar state</dt>
<dd>
<strong>Value:</strong>
<div>{barState}</div>
<button onClick={(event) => {event.preventDefault(); setBar(`bar updated at ${new Date().toLocaleString()}`)}}>Update Bar</button>
</dd>
<dt>Render context state:</dt>
<dd><pre>{JSON.stringify(this.state)}</pre></dd>
</dl>
)
}
}
ReactDOM.render(<FakeHook/>, document.getElementById('main'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<main id=main>loading or error occurred...</main>
Notice that state is stored based on the order the hook is called inside render. In real hooks, the render context is stored somewhere other than this.state, but hooks know how to get it, and you don't really care. Also, this is just an example, real hooks work slightly differently, but the concept is the same.
*: async functions and generators don't run all at once, and instead return a special object that lets the function run in multiple steps, waiting or pausing on await or yield.
I have been using React and D3 separately and now have a project where I need low level control over the plotting function of an application. Basically, I need to be able to go and fetch higher resolution data from a database as the user zooms in, and vice versa as the user zooms out, on a plot.
I have found a few methods to use D3 and React together. I wanted to try and keep all of my React code based around the hooks API as that is what is used for the rest of the code base. I am struggling to get the hooks equivalent for the specific cases that I am facing. The documentation on React hooks is great but I think my situation is more of an edge case and I have not seen any discussion relating to similar use cases to what I have.
The logic of the code is fairly straight forward:
I have a main container, call it App.js, that hold some state. App.js renders a Wrapper component (which is where the challenge is occurring) and then the Wrapper.js file simply creates the D3 Plot. The D3 plot is just typical D3 code for a line plot with some zoom events.
The Wrapper code:
import React, { Component } from 'react';
import Plot from './Plot'; // D3 plot
class Wrapper extends Component {
// Sets up plot when Wrapper first added to DOM tree
componentDidMount(){
this.setState({
plot: new Plot(this.refs.plot, this.props.data, this.props.updateData)
});
};
// Do not allow React to re-render the Wrapper and therefore the Plot
shouldComponentUpdate(){
return false;
};
// When the data prop changes, execute the update() method in the plot file
componentWillReceiveProps(nextProps){
this.state.plot.update(nextProps.data) // the .update() method calls props.updateData()
}
render(){
return <div ref="plot"></div>
}
}
export default Wrapper;
I have started putting together the hooks version below but I cannot come up with suitable emthods that meet the specific requirements of what I am after for the cases of the shouldComponentUpdate and componentWIllReceiveProps blocks.
hooks version:
import React, { useEffect, useRef } from 'react';
import Plot from './Plot'; // D3 plot
const Wrapper = props => {
// destruct props
const {
data = props.data,
fields = props.fields,
click = props.click
} = props
// initialise empty ref
const plotRef= useRef(null);
// ComponentDidMount translated to hooks
useEffect(() => {
new Plot(plotRef, data, updateData)
},[]) // empty array ensures the plot object is only created once on initial mounting
// shouldComponentUpdate
useEffect(...) // This won't work because render has already occurred before useEffect dependency array diffed?
// ComponentWIllReceiveProps
useEffect(() => {
plot.update(data)
}, [data]) // This won't work because component needs to have re-rendered
return (
<div ref= {plotRef}></div>
)
};
export default Wrapper;
What I am trying to achieve is blocking any rendering of the D3 chart after the initial mount, but then if the data props changes in the parent component, execute a method in the D3 class without allowing React to re-render the Wrapper component.
Is this at all possible or am I better off leaving this as a class based component?
I have been banging my head against a wall trying to get the logic without any success so any input would be greatly appreciated.
you can wrap your return value in React useMemo with suitable dependencies.
https://reactjs.org/docs/hooks-reference.html#usememo
We prefer to use functional statetless react components as much as possible (especially now with React Hooks).
However, I cannot figure out how to achieve the effect of binding a DevExtreme PivotGrid to a Chart (so that one gets a combined operation).
As you can see from their example: https://js.devexpress.com/Demos/WidgetsGallery/Demo/PivotGrid/ChartIntegration/React/MaterialTealDark/
They use Class components with ref's and then do when the combined Class component mounts:
componentDidMount() {
this._pivotGrid.bindChart(this._chart, {
dataFieldsDisplayMode: 'splitPanes',
alternateDataFields: false
});
}
We would rather not go that way. Is there some neat trick one can use with the new React useRef() hook, or by giving HTML doc id's to the chart so that we can bind the two components at some time after the initial load?
We are not purists here, so even slightly ugly solutions would do.
I can't think of anything but a simple change to doing it the Hooks way, aka put the code in the useEffect() hook and use useRef() to bind the refs:
// import useEffect and useRef hook from 'react',
import React, { useEffect, useRef() } from 'react'
function App() {
// initialize the ref variables with null as the initial value,
const pivotGrid = useRef(null);
const chart = useRef(null);
// useEffect runs after the layout paint...
useEffect(() => {
// do the binding on the references,
pivotGrid.current.bindChart(chart.current, {
dataFieldsDisplayMode: 'splitPanes',
alternateDataFields: false
});
}
,
// we pass an empty array to only run once after the component mount,
[])
// pass the refs variables to the components.
return (
<React.Fragment>
<Chart ref={chart}>
<Size height={320} />
<Tooltip enabled={true} customizeTooltip={customizeTooltip} />
<CommonSeriesSettings type={'bar'} />
<AdaptiveLayout width={450} />
</Chart>
<PivotGrid
id={'pivotgrid'}
dataSource={dataSource}
allowSortingBySummary={true}
allowFiltering={true}
showBorders={true}
showColumnTotals={false}
showColumnGrandTotals={false}
showRowTotals={false}
showRowGrandTotals={false}
ref={pivotGrid}
>
<FieldChooser enabled={true} height={400} />
</PivotGrid>
</React.Fragment>
);
}
I'm not 100% sure if you need to pass the ref.current or simply the ref itself . You can try both ways!
EDIT
Re: using useRef(), from the React docs:
Keep in mind that useRef doesn’t notify you when its content changes.
Mutating the .current property doesn’t cause a re-render. If you want
to run some code when React attaches or detaches a ref to a DOM node,
you may want to use a callback ref instead.