Entendendo Hooks para Animações no React
Animações são fundamentais para criar interfaces modernas e responsivas. No universo React, hooks especializados como useSpring, useAnimate e conceitos de física de movimento revolucionaram a forma como desenvolvemos transições fluidas. Diferente de CSS puro ou bibliotecas genéricas, esses hooks permitem sincronizar estado com animações de forma declarativa, respeitando o ciclo de vida dos componentes.
O grande diferencial desses hooks é que eles não apenas aplicam estilos visuais, mas integram-se profundamente com a lógica de componente. Você controla quando uma animação começa, para ou inverte com base em mudanças de estado reais. Além disso, eles lidam automaticamente com cancelamento de animações quando componentes desmontam, evitando memory leaks — um problema comum em implementações ingênuas.
useSpring: Animações Simples e Diretas
O Conceito Fundamental
useSpring é um hook da biblioteca React Spring que transforma valores de um estado inicial para um final com uma animação suave. Pense nele como um interpolador inteligente: você fornece os valores iniciais e finais, e ele calcula todos os frames intermediários baseado em uma configuração física de primavera (spring).
A "primavera" não é metafórica. Internamente, useSpring usa simulação de física: quanto mais rígida a spring (maior tension), mais rápido ela chega ao alvo; quanto maior o friction, mais ela desacelera. Isso cria movimentos naturais que respeitam leis da física, não frames arbitrários.
Exemplo Prático: Animação de Opacidade
import { useSpring, animated } from '@react-spring/web';
function FadeInBox() {
const [isVisible, setIsVisible] = React.useState(false);
const springProps = useSpring({
opacity: isVisible ? 1 : 0,
transform: isVisible ? 'translateY(0px)' : 'translateY(20px)',
config: {
tension: 280,
friction: 60,
},
});
return (
<div>
<button onClick={() => setIsVisible(!isVisible)}>
Toggle
</button>
<animated.div
style={springProps}
className="box"
>
Conteúdo que aparece com suavidade
</animated.div>
</div>
);
}
Note que não escrevemos CSS transitions. O componente animated.div recebe um objeto estilizado que é recalculado a cada frame. Quando isVisible muda, useSpring automaticamente anima a transição entre os valores antigos e novos.
Configurações de Física
A configuração config aceita presets ou valores customizados. Os presets (molasses, gentle, wobbly, stiff) são atalhos para combinações pré-testadas de tension e friction. Para controle fino:
const springProps = useSpring({
value: targetValue,
config: {
tension: 170, // rigidez: 1-300
friction: 26, // amortecimento: 1-100
mass: 1, // peso do objeto: 1-10
clamp: false, // previne overshoot
},
});
Aumentar tension sem ajustar friction causa "bouncing" — a animação ultrapassa o alvo e volta. Use clamp: true se quiser evitar esse efeito.
useAnimate: Controle Sequencial e Completo
Diferenciação: Por Que Não Só useSpring?
Enquanto useSpring é excelente para transições simples, useAnimate oferece controle narrativo. Você pode executar múltiplas animações em sequência, paralelo, com delays específicos, e até parar/resumir programaticamente. É o hook certo quando sua animação é mais "ação" que "transição de estado".
useAnimate pertence à mesma família React Spring, mas sua API é imperativa — você chama funções para disparar animações, não apenas passa valores finais. Isso é poderoso para tutoriais, onboardings, ou qualquer sequência complexa.
Exemplo: Sequência de Animações
import { useAnimate, stagger } from '@react-spring/web';
import React from 'react';
function SequentialAnimation() {
const [scope, animate] = useAnimate();
const handleAnimateSequence = async () => {
await animate(
'div.item',
{ opacity: 1, y: 0 },
{
duration: 0.6,
delay: stagger(0.1), // 100ms entre cada elemento
}
);
};
React.useEffect(() => {
// Começar com itens invisíveis
animate(
'div.item',
{ opacity: 0, y: 20 },
{ duration: 0 }
);
}, []);
return (
<div ref={scope}>
<button onClick={handleAnimateSequence}>
Animar Lista
</button>
<div className="item">Item 1</div>
<div className="item">Item 2</div>
<div className="item">Item 3</div>
</div>
);
}
O padrão aqui é: criar referência com useAnimate, usar animate() para modificar elementos selecionados via CSS, e stagger() para aplicar delays progressivos. Cada chamada await animate() representa um passo na sequência.
Animações Condicionais e Controle
function AnimatedForm() {
const [scope, animate] = useAnimate();
const [submitted, setSubmitted] = React.useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
// Animar erro ou sucesso baseado em validação
if (!isValid()) {
await animate(scope.current, {
x: [0, -10, 10, -10, 0],
}, {
duration: 0.4,
});
return;
}
// Sucesso: fade out
await animate(scope.current, {
opacity: 0,
scale: 0.8,
}, {
duration: 0.5,
});
setSubmitted(true);
};
return (
<form ref={scope} onSubmit={handleSubmit}>
<input type="email" />
<button>Enviar</button>
</form>
);
}
Aqui vemos useAnimate ser condicional: a animação de erro (shake) só ocorre se validação falhar. Isso é impossível com useSpring puro, que sempre anima quando estado muda.
Física de Movimento: Além das Primaveras
Princípios de Física Aplicados
Qualquer animação convincente obedece leis da física. Aceleração, velocidade, inércia — essas propriedades criam movimentos que parecem reais porque nossos olhos evoluíram para detectá-las. useSpring simula uma primavera ideal, mas existe mais no arsenal.
A física de movimento inclui:
- Momentum: quando você arrasta algo, ele continua se movendo mesmo após soltar (inércia)
- Damping: resistência do ar ou atrito que desacelera movimento
- Overshoot: excesso além do alvo antes de estabilizar
- Easing: aplicar funções matemáticas para controlar aceleração/desaceleração
Usando useSpring com Física Realística
import { useSpring, animated, config } from '@react-spring/web';
function DragBox() {
const [{ x, y }, api] = useSpring(() => ({
x: 0,
y: 0,
config: config.molasses, // pré-configurado para movimento pesado
}));
const handleMouseMove = (e) => {
const rect = e.currentTarget.getBoundingClientRect();
api.start({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
});
};
return (
<animated.div
onMouseMove={handleMouseMove}
style={{
width: '100px',
height: '100px',
background: 'blue',
transform: x.to((val) => `translate(${val}px, ${y.get()}px)`),
}}
/>
);
}
config.molasses é mais lento e pesado que config.stiff. O mouse se move para uma posição, mas o box "segue" com inércia, criando sensação de peso. Isso é feito variando friction e tension — não há mágica, apenas matemática.
Implementando Momentum Customizado
Para efeitos mais avançados como scroll momentum ou drag-to-dismiss, você pode combinar useSpring com detecção de velocidade:
import { useSpring, animated } from '@react-spring/web';
import { useGesture } from '@use-gesture/react';
function SwipeCard() {
const [{ x }, api] = useSpring(() => ({ x: 0 }));
const bind = useGesture({
onDrag: ({ offset: [ox], direction: [dx], velocity: [vx] }) => {
const isFlicked = Math.abs(vx) > 0.5; // velocidade suficiente
api.start({
x: isFlicked && Math.abs(ox) > 50
? dx > 0
? 500
: -500
: 0,
config: { velocity: vx, tension: 200, friction: 20 },
});
},
});
return (
<animated.div
{...bind()}
style={{ x, touchAction: 'none' }}
className="card"
>
Deslize para descartar
</animated.div>
);
}
Aqui, capturamos a velocidade do gesto e a passamos diretamente para useSpring. O hook continua o movimento com a inércia capturada, em vez de começar do zero. Resultado: movimento que sente-se natural e responsivo.
Combinando Tudo: Exemplo Completo de Interface Animada
Construindo um Menu Animado com Física
import React from 'react';
import { useSpring, useTrail, animated, config } from '@react-spring/web';
function AnimatedMenu() {
const [open, setOpen] = React.useState(false);
// Animação do botão hamburger
const hamburgerSpring = useSpring({
transform: open ? 'rotate(90deg)' : 'rotate(0deg)',
config: config.wobbly,
});
// Animação do background
const bgSpring = useSpring({
opacity: open ? 1 : 0,
pointerEvents: open ? 'auto' : 'none',
config: config.default,
});
// Items do menu com trail (animação escalonada)
const items = ['Home', 'Sobre', 'Projetos', 'Contato'];
const trail = useTrail(items.length, {
from: { opacity: 0, x: -40 },
to: { opacity: open ? 1 : 0, x: open ? 0 : -40 },
config: config.gentle,
});
return (
<>
<animated.button
style={hamburgerSpring}
onClick={() => setOpen(!open)}
>
☰
</animated.button>
<animated.div
style={bgSpring}
onClick={() => setOpen(false)}
className="menu-backdrop"
/>
<animated.nav
style={{
opacity: open ? 1 : 0,
y: open ? 0 : -20,
}}
className="menu"
>
{trail.map((style, index) => (
<animated.a
key={index}
style={style}
href="#"
>
{items[index]}
</animated.a>
))}
</animated.nav>
</>
);
}
Este exemplo combina três técnicas: useSpring para o botão, outro useSpring para o backdrop, e useTrail (que é um array de springs) para itens do menu. Cada item entra em sequência suave. Quando o menu fecha, tudo volta inversamente. Tudo sincronizado via estado React.
Conclusão
Três aprendizados centrais ficam claros após dominar esses hooks:
-
Hooks de animação não são decoração — são ferramentas de controle de estado visual que integram-se ao ciclo de vida React, evitando bugs de sincronização e memory leaks que animações CSS puras frequentemente causam.
-
Física de movimento real cria experiências credíveis — usar
config.molassesou ajustartensionefrictionnão é capricho, é fundamental; movimentos que respeitam inércia e damping parecem "vivos" para nossos olhos, enquanto animações lineares parecem robóticas. -
Escolha a ferramenta certa —
useSpringpara transições reativas a estado,useAnimatepara sequências narrativas e controle imperativo, e sempre considere velocidade de gesto (momentum) ao trabalhar com interações baseadas em arraste.