Jump to content
Search Community

Storing Timeline to State Hook (not playing)

maiya-22 test
Moderator Tag

Recommended Posts

Hi,

I am trying to store a timeline to the useState hook in react, so that I use the state variable to control the timeline. I have been able to get this to work easily with React class Components, via storing a timeline to the state, but it is not working in useState for some reason.

 

Does anyone know what I am doing wrong here? Thank you for any pointers!

 

In my code example, onClick events on buttons are doing things that other events might do, just so it keeps it simple.

 

Desired behavior:

 

1. Click first button, and it stores an array of classNames to a state hook.  The JSX then uses this array to render elements (in this case it is divs).

2. Click the second button, and it calls a function that creates a timeline. It passes the array of classNames to this function, so that they will be used in the green sock timeline.

3. Click the third button and play the timeline.

 

Actual behavior:

 

Step three is not working. The timeline logs to the console, showing that it is stored to the state hook, but won't play.  If I create the timeline in the actual onClick listener of the button (and pass the classNames stored on state to it), it does work. But this solution is not good because you are not storing the timeline anywhere to control, etc.

 

This code should work, if you import it with any react app that has greensock installed (and uuid for keynames, but that can be removed for testing). Also, starter pen for green sock and codepen isn't working, so if anyone knows how to easily import the TimelineMax into codepen, copy and pasting this code should work right away.

 

import React, { useState } from "react";
import { v4 as uuid } from "uuid";
import { CSSPlugin, TimelineMax } from "gsap/all";
const plugins = [CSSPlugin];
 
const createTimeline = (elements) => {
// add a dot to the class names
let elementClassSelectors = elements.map((className) => `.${className}`);
const tl = new TimelineMax({ paused: true });
tl.to(elementClassSelectors, 2, { x: 100 });
return tl;
};
 
export function AnimatedComponent() {
let [timeline, setTimeline] = useState(null);
let [elementsToAnimate, setElementsToAnimate] = useState([]);
 
return (
<div className="component-frame">
<div className="display-animation">
{elementsToAnimate.map((elementClassName, i) => {
// could use useRef here, but instead just adding classNames that can be used in green sock
return (
<div className={`image ${elementClassName}`} key={uuid()}>
I am image {i}
</div>
);
})}
</div>
<div
className="controls"
style={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
margin: "auto",
}}
>
{/*
These functions will actually be triggered by various events. But have
them fired by buttons for now. */}
<button
onClick={(e) => {
e.preventDefault();
let elementClassNames = [
"some-image",
"some-other-image",
"another-image",
];
setElementsToAnimate(elementClassNames);
}}
>
CLICK FIRST: store new elements to state
</button>
<button
onClick={(e) => {
e.preventDefault();
let newTimeline = createTimeline(elementsToAnimate);
setTimeline(newTimeline);
}}
>
CLICK SECOND:create timeline and store it to state (seems to be
working)
</button>
<button
onClick={(e) => {
e.preventDefault();
console.log("see if the timeline is on the state:", timeline);
timeline.play();
}}
>
CLICK THIRD: play the timeline on the state (not working)
</button>
<button
onClick={(e) => {
e.preventDefault();
let temporaryTimeline = createTimeline(elementsToAnimate);
temporaryTimeline.play();
}}
>
CLICK FOURTH: create timeline and don't store it to state (working,
but not good/ need the timeline on the state)
</button>
</div>
</div>
);
}
Link to comment
Share on other sites

Hi and welcome to the GreenSock forums.

 

The best approach to keep the GSAP instance through re-renders without creating it again and again is to use useRef, also it is recommended to create the GSAP instance with the useEffect hook, passing an empty array, in order to ensure that the elements targeted in the GSAP instance are actually in the DOM:

 

import React { useRef, useEffect } from "react";
import { gsap } from "gsap";

const MyComponent () => {
  const tl = useRef(null);
  const myDomElement = useRef(null);
  
  useEffect(() => {
    tl.current = gsap.timeline({/* config here */})
      .to(myDomElement.current, { /* config here */ });
    // finally return this function to clean up after the component is unmounted
    return () => {
      tl.current.kill();
    };
  }, []);
  
  // then in an event handler
  const myClickHandler = (e) => {
    tl.current.play();
  };
};

You can also use the regular ref callback if you want to store a reference to the DOM element as well.

 

Finally if possible, create a live editable sample in Codesandbox or Stackblitz in order to take a better look. Also this makes debugging and helping with issues a lot faster.

 

Happy Tweening!!!

  • Like 3
Link to comment
Share on other sites

Thank you for such a quick reply, and the help!  That seems to have done the trick.

I didn't think of the fact that refs are able to be accessed after the JSX is rendered, similar to useState. That is really helpful. It's still confusing that a timeline can be stored to a class component's state, but not a state hook. (I don't get why, but oh well).

 

 

1 hour ago, Rodrigo said:

Finally if possible, create a live editable sample in Codesandbox or Stackblitz in order to take a better look. Also this makes debugging and helping with issues a lot faster

I was looking for a gsap starter, to figure out how to import the TimelineMax into the codepen editor. The one I found on gsap's codepen home page isn't working, but I'll keep looking. Does anyone know of any others? 

Link to comment
Share on other sites

1 hour ago, maiya-22 said:

I didn't think of the fact that refs are able to be accessed after the JSX is rendered, similar to useState. That is really helpful. It's still confusing that a timeline can be stored to a class component's state, but not a state hook. (I don't get why, but oh well).

Well, actually state is not really the ideal place for a GSAP instance or an instance of any other library/framework. In a class-type constructor is better to add them as properties of the class instance:

class MyComponent extends React.Component {
  constructor(){
    super();
    this.tl = gsap.timeline({ paused: true });
  }
  
  // Then later in the code it can be accessed
}

This is also true, for example if you're working with Vue, is better to add the GSAP instance when the component is created as a property of the component's instance. But I won't extend on this subject in order to not create confusion. The main idea is that state is for handling reactive data, that is, data that is updated and has an effect in the resulting UI rendered by the component. While this can be considered true in the case of GSAP, keep in mind that the change GSAP does is mostly in the styles of a DOM element, so it shouldn't be added in the state.

1 hour ago, maiya-22 said:

I was looking for a gsap starter, to figure out how to import the TimelineMax into the codepen editor. The one I found on gsap's codepen home page isn't working, but I'll keep looking. Does anyone know of any others?

Codesandbox is a good alternative: https://codesandbox.io/

 

This is a super simple example of using GSAP with the hooks API:

 

https://codesandbox.io/s/gsap-toggle-instance-with-hooks-t9uqr

 

Happy Tweening!!!

 

  • Like 3
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...