DEV Community

Cover image for Animaciones con GSAP + React 🌟
Franklin Martinez
Franklin Martinez

Posted on

Animaciones con GSAP + React 🌟

En esta ocasión aprenderemos como usar la librería de GSAP para realizar animaciones junto con React JS.

Nota: no es un tutorial de gsap, por lo que debes tener ciertas nociones de esta librería.

El propósito es que sepas complementar con buenas practicas las animaciones dentro del ecosistema de React JS.

 

Tabla de contenido.

📌 Tecnologías a utilizar.

📌 Creando el proyecto.

📌 Usando GSAP por primera vez.

📌 Conociendo gsap.context().

📌 Usando lineas de tiempo.

📌 Animar con interacciones.

📌 Evitar el flash del contenido que aun no esta estilizado.

📌 Conclusión.

 

☄️ Tecnologías a utilizar.

  • ▶️ React JS (version 18)
  • ▶️ GSAP 3
  • ▶️ Vite JS
  • ▶️ TypeScript
  • ▶️ CSS vanilla (Los estilos los encuentras en el repositorio al final de este post)

☄️ Creando el proyecto.

Al proyecto le colocaremos el nombre de: gsap-react (opcional, tu le puedes poner el nombre que gustes).

npm init vite@latest
Enter fullscreen mode Exit fullscreen mode

Creamos el proyecto con Vite JS y seleccionamos React con TypeScript.

Luego ejecutamos el siguiente comando para navegar al directorio que se acaba de crear.

cd gsap-react
Enter fullscreen mode Exit fullscreen mode

Luego instalamos las dependencias.

npm install
Enter fullscreen mode Exit fullscreen mode

Después abrimos el proyecto en un editor de código (en mi caso VS code).

code .
Enter fullscreen mode Exit fullscreen mode

☄️ Usando GSAP por primera vez.

Dentro del archivo src/App.tsx borramos todo y creamos un componente que muestre un div con un hello world

El componente Title no es tan importante

const App = () => {
    return (
        <>
            <Title />
            <div className="square">Hello World</div>
        </>
    )
}
export default App
Enter fullscreen mode Exit fullscreen mode

Asi se vería.

Image description

Ahora instalemos GSAP.

npm install gsap
Enter fullscreen mode Exit fullscreen mode

Para usar gsap y animar algún elemento podemos hacerlo de la siguiente manera...

Primero importamos gsap.

Luego, gsap tiene varias funciones para realizar animaciones.

-to: comenzará en el estado actual del elemento y animará "hacia" los valores definidos en la interpolación.
-from: es como un .to() al revés, que anima "desde" los valores definidos en la interpolación y termina en el estado actual del elemento.
-fromTo: Se definen los valores iniciales y finales.
-set: establece inmediatamente las propiedades (sin animación).

En este caso usaremos el más común que es la función to, ya que todas las funciones se usan de la misma manera con dos parámetros (a excepción del fromTo que recibe 3).

  • El primer parámetro es el elemento o elementos a animar, este parámetro puede ser un string (el id del elemento HTML o incluso hasta un selector complejo de CSS) o puede ser un elemento HTML o un array de elementos HTML.

  • El segundo parámetro es un objeto con las variables que quieres animar y que valor les quieres dar.

import { gsap } from "gsap";


const App = () => {

 gsap.to( ".square", { rotate: 360 })

  return (
    <>
      <div className="square">Hello World</div>
    </>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

Pero detente, esto que acabas de ver, es una mala practica en React JS 😞...

Primero, los efectos secundarios como las animaciones, deben ser ejecutadas dentro del hook useEffect como buena practica y asi evitar comportamientos no esperados.

Segundo, nota que el primer parámetro colocamos ".square", lo cual no esta mal, incluso funcionaria pero, React nos recomienda el no acceder a elementos del DOM de esa manera, para eso usaríamos otro hook, que es el useRef.

Entonces asi quedaría el código de nuestra primera animación, cuando la app comience, se ejecutara la animación que debería rotar en 360 grados el div con el Hello World.

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

const App = () => {

  const app = useRef<HTMLDivElement>(null);


  useEffect(() => {

    gsap.to(app.current, { rotate: 360, duration: 5 })

  }, [])

  return (
    <>
      <div ref={app}>Hello World</div>
    </>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

☄️ Conociendo gsap.context().

Bueno ya vimos como hacer una animación, usando las buenas practicas. Pero que tal si queremos hacer otras animaciones a distintos elementos, ¿tenemos que crear mas referencias?. Pues no es necesario, gracias a gsap.context.

Básicamente, solo creas una referencia, que actuara como padre, conteniendo los elementos que quieras animar.

La función gsap.context tiene dos parámetros.

  • Un callback, donde básicamente realizaras las animaciones, (ten en cuanta que solo seleccionaras los elementos descendientes).

  • El elemento que actuara como contenedor.

Pero ahora vamos paso a paso:

1 - Tenemos definido este componente

const App = () => {

  return (
    <div>
      <div className="square">Hello World</div>
    </div>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

2 - Creamos una nueva referencia hacia el elemento padre que encierra todos los demás elementos.

import { useRef } from "react";

const App = () => {

  const app = useRef<HTMLDivElement>(null);

  return (
    <div ref={app}>
      <div className="square">Hello World</div>
    </div>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

3 - Creamos un efecto.

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

const App = () => {

  const app = useRef<HTMLDivElement>(null);

  useEffect(() => {


  }, [])


  return (
    <div ref={app}>
      <div className="square">Hello World</div>
    </div>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

4 - Dentro del efecto, creamos una nueva variable, a la cual le asignaremos la función context de gsap. Esto es porque al final necesitaremos limpiar las animaciones y que no se sigan ejecutando, por eso crearemos la variable ctx que usaremos mas adelante.

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

const App = () => {

  const app = useRef<HTMLDivElement>(null);

  useEffect(() => {

    let ctx = gsap.context()

  }, [])


  return (
    <div ref={app}>
      <div className="square">Hello World</div>
    </div>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

5 - La función context recibe dos parámetros, el callback (donde estableceremos las demás animaciones), y el elemento (este elemento debe contener elementos descendientes).

Nota que el segundo parámetro le estamos pasando toda la referencia como tal, No le pasamos el app.current

De una vez ejecutamos la función de limpieza del useEffect. y usamos la variable ctx que declaramos anteriormente y ejecutamos el método revert() para limpiar las animaciones y que no se ejecuten

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

const App = () => {

  const app = useRef<HTMLDivElement>(null);

  useEffect(() => {

    let ctx = gsap.context(() => {}, app);

    return () => ctx.revert();

  }, [])


  return (
    <div ref={app}>
      <div className="square">Hello World</div>
    </div>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

6 - Ahora si, podemos realizar las animaciones dentro del callback definido en método context

Realizamos una animación como te lo mostré anteriormente

Si notas la animación dentro de callback del context, es algo contradictorio a las buenas practicas que mencione antes, ya que volvemos obtener el elemento mediante su clase. 🤔

Pero la ventaja es que no tendrás que crear una referencia por cada elemento que quieras animar 😉

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

const App = () => {

  const app = useRef<HTMLDivElement>(null);

  useEffect(() => {

    let ctx = gsap.context(() => {

      gsap.to(".square", { rotate: 360, duration: 5 });
      //gsap.to(".square2", { rotate: 360, duration: 5 });
      //gsap.to(".square3", { rotate: 360, duration: 5 });

    }, app);

    return () => ctx.revert();

  }, [])


  return (
    <div ref={app}>
      <div className="square">Hello World</div>
    </div>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

Nota que si tu quieres animar un elemento (.square2) que no es descendiente del elemento padre que tiene la referencia, no podrás animarlo en el context, al menos que muevas dicho elemento dentro del elemento con la referencia para que se convierta en un elemento hijo.

El elemento con la clase .square2 no se animara porque no es un elemento hijo del elemento que tiene la ref

useEffect(() => {

let ctx = gsap.context(() => {

    gsap.to(".square", { rotate: 360, duration: 5 });

    gsap.to(".square2", { rotate: 360, duration: 5 }); // Don't work ❌

}, app);

return () => ctx.revert();

}, [])

<div ref={app}>
    <div className="square">Hello World</div>
</div>

<div className="square2">Hello World</div>
Enter fullscreen mode Exit fullscreen mode

☄️ Usando lineas de tiempo.

Las lineas de tiempo nos ayudaran a crear animaciones en secuencia.

Y para crear una linea de tiempo, lo haremos de la siguiente manera.

Tenemos prácticamente todo el código anterior, donde usamos la función context de gsap.
Solamente que el callback por el momento esta vació, y agregamos un nuevo elemento div con la clase square2 y dentro un Hello World 2

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

const App = () => {

  const app = useRef<HTMLDivElement>(null);

  useEffect(() => {

    let ctx = gsap.context(() => {}, app);

    return () => ctx.revert();

  }, [])


  return (
    <div ref={app}>
      <div className="square">Hello World</div>
      <div className="square2">Hello World 2</div>
    </div>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

Para crear la linea del tiempo, usaremos una nueva referencia.

  const tl = useRef<GSAPTimeline>()
Enter fullscreen mode Exit fullscreen mode

Ahora dentro del callback del context, a la ref tl le asignaremos la función timeline de gsap.

Esto nos servirá para evitar crear una nueva linea de tiempo cada vez que se renderize el componente.

let ctx = gsap.context(() => {

    tl.current = gsap.timeline()

}, app);
Enter fullscreen mode Exit fullscreen mode

Entonces, para agregar una animación a un elemento, solamente ejecutamos el tipo de animación con sus parámetros requeridos.

De la siguiente manera:

let ctx = gsap.context(() => {

    tl.current = gsap.timeline().to(".square", { rotate: 360 }).to(".square2", { x: 200});

}, app);
Enter fullscreen mode Exit fullscreen mode

Para una mejor lectura de código podemos colocar el código asi:

let ctx = gsap.context(() => {

    tl.current = gsap.timeline()
      .to(".square", { rotate: 360 })
      .to(".square2", { x: 200 })

}, app);
Enter fullscreen mode Exit fullscreen mode

Esto significa que primero se ejecuta la animación al elemento con la clase .square y hasta que esta animación termine se ejecutara la siguiente animación que es para el elemento con la clase .square2.

Claro que la duración de la animación puede configurarse para que se ejecuten al mismo tiempo o algo por el estilo.

El código quedaría asi:

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

const App = () => {

  const app = useRef<HTMLDivElement>(null);
  const tl = useRef<GSAPTimeline>()

  useEffect(() => {

    let ctx = gsap.context(() => {

      tl.current = gsap.timeline()
        .to(".square", { rotate: 360 })
        .to(".square2", { x: 200 });

    }, app);

    return () => ctx.revert()

  }, [])


  return (
    <div ref={app}>
      <div className="square">Hello World</div>
      <div className="square2">Hello World 2</div>
    </div>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

Image description

☄️ Animar con interacciones.

También puedes ejecutar las animaciones mediante interacciones que se hagan con los elementos, no solamente al iniciar tu aplicación y en el useEffect.

Por ejemplo, mediante el evento click a un elemento puedes disparar alguna animación.

Nota que la función handleClick recibe el evento. Por lo cual tendremos acceso al elemento que le estamos dando click.

Y ese elemento (e.target) es lo que le pasaremos a la animación "to" de gsap

import { gsap } from "gsap";

const App = () => {

  const handleClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {

    gsap.to(e.target, { rotation: '50', yoyo: true, repeat: 1 })
  }

  return (
    <>
      <div onClick={handleClick} >Hello World</div>
    </>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

No solamente lo puedes hacer con un evento click, también con otros diversos eventos como onMouseEnter u onMouseLeave

import { gsap } from "gsap";

const App = () => {

  const handleClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
    gsap.to(e.target, { rotation: '50', yoyo: true, repeat: 1 })
  }

  const onEnter = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
    gsap.to(e.target, { scale: 1.2 });
  };

  const onLeave = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
    gsap.to(e.target, { scale: 1 });
  };

  return (
    <>
      <div 
        onMouseEnter={onEnter} 
        onMouseLeave={onLeave} 
        onClick={handleClick}

      >Hello World</div>
    </>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

Image description

☄️ Evitar el flash del contenido que aun no esta estilizado.

Seguramente haz notado que al iniciar la aplicación y debes ejecutar una animación al inicio, notas un pequeño destello del contenido que quieres animar pero sin estilos css, después de eso ya se ejecuta la animación correspondiente.

A lo largo de este trayecto has visto que usamos useEffect para ejecutar las animaciones cuando inicia la aplicación. Pero useEffect se ejecuta después de que se haya pintado el DOM y es por eso que ocurre ese pequeño destello no deseado.

Para evitar ese destello, en vez de usar useEffect usaremos useLayoutEffect el cual funciona exactamente igual que useEffect pero la diferencia es que el useLayoutEffect se ejecuta antes de que se pinte el DOM.

Este es un ejemplo tomado de la documentación de gsap, y es exactamente lo que queremos evitar.

Image description

Lo evitamos usando useLayoutEffect

import { gsap } from "gsap";
import { useLayoutEffect, useRef } from "react";

const App = () => {

  const app = useRef<HTMLDivElement>(null);

  useLayoutEffect(() => {

    let ctx = gsap.context(() => {

      gsap.to(".square", { rotate: 360, duration: 5 });

    }, app);

    return () => ctx.revert();

  }, [])


  return (
    <div ref={app}>
      <div className="square">Hello World</div>
    </div>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

Image description

☄️ Conclusión.

La librería de GSAP, sin duda es muy interesante, por lo que espero haberte ayudado a entender como se usa con React JS siguiendo buenas practicas y consejos. 🙌

Por cierto otro consejo seria que no animaras todo. También solo deberías animar propiedades como las transformaciones u opacidad y evita animar las propiedades como filter o boxShadow ya que consumen mucha CPU en los navegadores.

Asegúrate que las animaciones funcionen en dispositivos de gama baja.

Si conoces alguna otra forma distinta o mejor de realizar animaciones con esta librería u otra, con gusto puedes comentarla.

Te invito a que revises mi portafolio en caso de que estés interesado en contactarme para algún proyecto! Franklin Martinez Lucas

🔵 No olvides seguirme también en twitter: @Frankomtz361

Top comments (1)

Collapse
 
flash010603 profile image
Usuario163

Que buena librería la de GSAP! buen post!