Animaciones con GSAP + React 🌟
Tags: react, javascript, spanish, tutorial
Published: 2023-02-24 15:59:38

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.

&nbsp;

## Tabla de contenido.
> 📌 [Tecnologías a utilizar.](#1)  
> 📌 [Creando el proyecto.](#2)  
> 📌 [Usando GSAP por primera vez.](#3)  
> 📌 [Conociendo gsap.context().](#4)  
> 📌 [Usando lineas de tiempo.](#5)  
> 📌 [Animar con interacciones.](#6)  
> 📌 [Evitar el flash del contenido que aun no esta estilizado.](#7)  
> 📌 [Conclusión.](#8)    

&nbsp;


## ☄️ Tecnologías a utilizar. <a id="1"></a>

- ▶️ React JS (version 18)
- ▶️ [GSAP 3](https://greensock.com/gsap/)
- ▶️ Vite JS
- ▶️ TypeScript
- ▶️ CSS vanilla (Los estilos los encuentras en el repositorio al final de este post) 


## ☄️ Creando el proyecto. <a id="2"></a>

Al proyecto le colocaremos el nombre de: **`gsap-react`** (opcional, tu le puedes poner el nombre que gustes).
```bash
npm init vite@latest
```
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.

```bash
cd gsap-react
```
Luego instalamos las dependencias.
```bash
npm install
```
Después abrimos el proyecto en un editor de código (en mi caso VS code).
```bash
code .
```

## ☄️ Usando GSAP por primera vez. <a id="3"></a>

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

```tsx
const App = () => {
    return (
        <>
            <Title />
            <div className="square">Hello World</div>
        </>
    )
}
export default App
```

Asi se vería.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wwh1awea9tm89jas3hrc.gif)


Ahora instalemos GSAP.

```bash
npm install gsap
```

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.


```tsx
import { gsap } from "gsap";


const App = () => {
    
 gsap.to( ".square", { rotate: 360 })

  return (
    <>
      <div className="square">Hello World</div>
    </>
  )
}
export default App
```


> 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. <a id="first-animation"></a>

```tsx
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
```

## ☄️ Conociendo gsap.context(). <a id="4"></a>

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


```tsx
const App = () => {

  return (
    <div>
      <div className="square">Hello World</div>
    </div>
  )
}
export default App
```

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

```tsx
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
```

3 - Creamos un efecto. 

```tsx
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
```

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.

```tsx
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
```

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

```tsx
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
```

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

Realizamos una animación como te lo [mostré anteriormente](#first-animation)

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** 😉

```tsx
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
```

> 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**

```tsx
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>
```

## ☄️ Usando lineas de tiempo. <a id="5"></a>
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

```tsx
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
```

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

```tsx
  const tl = useRef<GSAPTimeline>()
```

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.

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

    tl.current = gsap.timeline()

}, app);
```

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:

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

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

}, app);
```

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

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

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

}, app);
```

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:

```tsx
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
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v0g3m53y82pn39cewfsv.gif)

## ☄️ Animar con interacciones. <a id="6"></a>

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**

```tsx
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
```

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

```tsx
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
```


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/awrpvi0kemokju1n2hlh.gif)

## ☄️ Evitar el flash del contenido que aun no esta estilizado. <a id="7"></a>

 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n05gisrbpk9t857wy6zq.gif)


Lo evitamos usando useLayoutEffect


```tsx
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
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fsuyjw60bleul9s8b67v.gif)


## ☄️ Conclusión. <a id="8"></a>

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](https://franklin-dev.netlify.app/)**

> 🔵 No olvides seguirme también en twitter: [**@Frankomtz361**](https://twitter.com/Frankomtz361)

{%embed https://twitter.com/Frankomtz361/status/1575329958674796547?s=20&t=AVAVQ1am_AkFb2TyMo9S-A %}