Bài 26.3: Framer Motion trong TypeScript

Chào mừng bạn đến với bài viết chuyên sâu về việc mang sự sống động vào ứng dụng React của bạn bằng Framer Motion, kết hợp với sự mạnh mẽ và an toàn của TypeScript. Animation không chỉ làm cho giao diện người dùng trở nên đẹp mắt hơn mà còn giúp cải thiện trải nghiệm người dùng bằng cách cung cấp phản hồi trực quan và dẫn dắt sự chú ý của họ. Framer Motion là một thư viện animation cực kỳ phổ biến và mạnh mẽ cho React, làm cho việc tạo ra các hiệu ứng động phức tạp trở nên đơn giản đáng ngạc nhiên.

Tại sao lại là Framer Motion?

Có rất nhiều thư viện animation cho React, vậy tại sao Framer Motion lại nổi bật?

  • Dễ sử dụng: Cú pháp trực quan, tập trung vào các component React. Bạn chỉ cần thêm các props vào component của mình.
  • Mạnh mẽ: Hỗ trợ nhiều loại animation (layout, gestures, scroll, v.v.), vật lý (spring physics), và các tính năng nâng cao như variants để quản lý state animation.
  • Hiệu suất cao: Được tối ưu hóa để chạy mượt mà, ngay cả trên các thiết bị di động.
  • Hỗ trợ TypeScript tuyệt vời: Đây là điểm cộng lớn mà chúng ta sẽ đi sâu vào.
Sức mạnh của TypeScript khi kết hợp với Framer Motion

Khi làm việc với animation, đặc biệt là với một thư viện nhiều tính năng như Framer Motion, bạn sẽ phải xử lý rất nhiều thuộc tính style và cấu hình animation. TypeScript mang lại những lợi ích to lớn:

  1. An toàn kiểu dữ liệu: TypeScript giúp bạn bắt lỗi ngay tại thời điểm biên dịch thay vì lúc chạy. Bạn sẽ biết ngay nếu nhập sai tên thuộc tính animation hoặc sử dụng sai kiểu giá trị.
  2. Tự động hoàn thành (Autocompletion): Trình chỉnh sửa code của bạn (VS Code, WebStorm,...) sẽ cung cấp gợi ý đầy đủ các thuộc tính mà bạn có thể sử dụng với motion component, giúp tăng tốc độ code và giảm lỗi chính tả.
  3. Hiểu rõ cấu trúc: Khi làm việc với variants hoặc các props phức tạp khác, TypeScript giúp bạn hiểu rõ cấu trúc dữ liệu cần thiết.

Framer Motion được viết bằng TypeScript và cung cấp định nghĩa kiểu dữ liệu hoàn chỉnh, làm cho trải nghiệm phát triển với TypeScript trở nên mượt màtin cậy.

Bắt đầu với Framer Motion và TypeScript

Đầu tiên, bạn cần cài đặt thư viện:

npm install framer-motion
# hoặc
yarn add framer-motion

Sau đó, bạn có thể bắt đầu sử dụng. Cú pháp cơ bản là thêm tiền tố motion. vào bất kỳ component HTML hoặc React nào bạn muốn tạo animation.

import { motion } from "framer-motion";

function MyComponent() {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }} // Trạng thái ban đầu
      animate={{ opacity: 1, y: 0 }}   // Trạng thái đích (khi component mount)
      transition={{ duration: 0.5 }}  // Cấu hình chuyển động
      className="box"
    >
      Xin chào, Framer Motion!
    </motion.div>
  );
}

Giải thích:

  • Chúng ta import motion từ thư viện framer-motion.
  • motion.div tạo ra một thẻ div có khả năng animation. Tương tự, bạn có motion.span, motion.button, motion.img, motion.svg, motion.path, hoặc thậm chí là motion(CustomReactComponent).
  • initial: Định nghĩa trạng thái bắt đầu của animation. Ở đây là opacity: 0 (trong suốt) và y: 20 (dịch xuống 20px). TypeScript giúp bạn gợi ý các thuộc tính CSS hoặc transform hợp lệ.
  • animate: Định nghĩa trạng thái kết thúc của animation. Khi component được render, nó sẽ tự động chuyển từ trạng thái initial đến animate. Ở đây là opacity: 1 (hiện rõ) và y: 0 (trở về vị trí ban đầu).
  • transition: Cấu hình cách thức animation diễn ra. duration: 0.5 nghĩa là animation này sẽ kéo dài 0.5 giây. TypeScript sẽ gợi ý các tùy chọn khác như type ('spring', 'tween', 'inertia'), delay, ease, v.v.

Với TypeScript, khi bạn gõ initial={{ }} hoặc animate={{ }}, bạn sẽ nhận được gợi ý tuyệt vời về các thuộc tính style và transform có thể animate, cùng với kiểu dữ liệu mong đợi cho từng thuộc tính. Điều này giúp bạn tránh gõ sai 'opacit' thay vì 'opacity' hoặc gán một string vào thuộc tính cần number.

Ví dụ: Hiệu ứng di chuột (Hover)

Framer Motion làm cho các hiệu ứng tương tác trở nên cực kỳ đơn giản với các props như whileHover, whileTap, whileFocus, v.v.

import { motion } from "framer-motion";
import React from "react"; // Import React nếu cần JSX

function HoverScaleButton() {
  return (
    <motion.button
      whileHover={{ scale: 1.1, backgroundColor: "#3498db" }} // Scale up and change color on hover
      whileTap={{ scale: 0.9 }} // Scale down slightly on tap/click
      transition={{ type: "spring", stiffness: 400, damping: 10 }} // Use spring physics
      className="my-button"
      style={{
        padding: '10px 20px',
        fontSize: '16px',
        border: 'none',
        borderRadius: '5px',
        cursor: 'pointer',
        backgroundColor: '#2ecc71',
        color: 'white'
      }}
    >
      Hover Me!
    </motion.button>
  );
}

Giải thích:

  • motion.button là một thẻ <button> có khả năng animation.
  • whileHover: Định nghĩa animation sẽ chạy khi con trỏ chuột di qua component. Ở đây, nó sẽ phóng to scale: 1.1 và đổi màu nền backgroundColor: "#3498db".
  • whileTap: Định nghĩa animation sẽ chạy khi component bị nhấn (click hoặc tap trên di động). Ở đây, nó thu nhỏ lại một chút scale: 0.9.
  • transition: Chúng ta sử dụng type: "spring" để tạo hiệu ứng nhún (spring physics), làm cho animation trở nên tự nhiên và mượt mà hơn. stiffnessdamping là các tham số điều chỉnh hiệu ứng spring.
  • TypeScript đảm bảo rằng các thuộc tính bên trong whileHover, whileTap, và transition đều hợp lệ và đúng kiểu.
Ví dụ: Sử dụng Variants cho Animation phức tạp hơn

variants là một tính năng mạnh mẽ trong Framer Motion giúp bạn quản lý các trạng thái animation một cách rõ ràng và dễ dàng tạo animation cho các danh sách (list orchestration).

import { motion, Variants } from "framer-motion";
import React from "react";

// Định nghĩa variants bằng TypeScript Interface hoặc type
// Điều này giúp đảm bảo cấu trúc của variants là chính xác
interface ContainerVariants extends Variants {
  hidden: {};
  visible: {
    transition: {
      staggerChildren: number; // staggerChildren là thuộc tính của transition
    };
  };
}

interface ItemVariants extends Variants {
  hidden: { opacity: number; y: number };
  visible: { opacity: number; y: number };
}


const containerVariants: ContainerVariants = {
  hidden: { opacity: 1 },
  visible: {
    opacity: 1,
    transition: {
      delayChildren: 0.3, // Độ trễ trước khi animation của các con bắt đầu
      staggerChildren: 0.2 // Độ trễ giữa các item con
    }
  }
};

const itemVariants: ItemVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: { opacity: 1, y: 0 }
};

function ListItemAnimation() {
  return (
    <motion.ul
      variants={containerVariants} // Áp dụng variants cho container
      initial="hidden"          // Trạng thái ban đầu của container
      animate="visible"         // Trạng thái đích của container
      style={{ listStyle: 'none', padding: 0 }}
    >
      {["Item 1", "Item 2", "Item 3", "Item 4"].map((item, index) => (
        <motion.li
          key={index}
          variants={itemVariants} // Áp dụng variants cho từng item
          style={{
            backgroundColor: '#ecf0f1',
            margin: '10px 0',
            padding: '10px',
            borderRadius: '5px'
          }}
        >
          {item}
        </motion.li>
      ))}
    </motion.ul>
  );
}

Giải thích:

  • Chúng ta định nghĩa hai object variants: containerVariants cho thẻ <ul>itemVariants cho từng thẻ <li>.
  • Mỗi object variants chứa các key biểu thị "trạng thái" của animation (ví dụ: "hidden", "visible"). Giá trị của mỗi key là một object style/transform hoặc một object transition.
  • TypeScript: Chúng ta sử dụng Interface (ContainerVariants, ItemVariants) để định nghĩa cấu trúc mong đợi của các object variants. Điều này rất hữu ích vì cấu trúc của variants có thể phức tạp (ví dụ: transition nằm lồng bên trong trạng thái), và TypeScript giúp bạn đảm bảo bạn đang định nghĩa đúng.
  • Trên thẻ motion.ul (container), chúng ta gán variants={containerVariants}, initial="hidden", và animate="visible". Khi container chuyển từ trạng thái "hidden" sang "visible", nó sẽ kích hoạt animation của chính nó (trong trường hợp này là opacity của container không đổi, nhưng transition chứa staggerChildren).
  • Trên mỗi thẻ motion.li (item), chúng ta gán variants={itemVariants}. Bởi vì chúng nằm trong một motion component cha sử dụng variants và có staggerChildren trong transition, Framer Motion sẽ tự động điều phối animation của từng item con dựa trên itemVariants.
  • staggerChildren: Thuộc tính trong transition của container variants. Nó tạo ra một độ trễ giữa thời điểm bắt đầu animation của các item con, tạo ra hiệu ứng "lượn sóng" đẹp mắt.

Ví dụ này cho thấy sức mạnh của variants trong việc quản lý state animation và cách Framer Motion xử lý việc phối hợp animation cho các danh sách một cách tự động. Và TypeScript giúp bạn định nghĩa các variants này một cách an toàn và có cấu trúc.

Sử dụng Hooks để điều khiển Animation

Framer Motion cung cấp các hooks mạnh mẽ để kiểm soát animation một cách thủ công hoặc phản ứng với các sự kiện phức tạp hơn. Một hook phổ biến là useAnimationControls (hoặc useAnimate trong các phiên bản mới hơn).

import { motion, useAnimationControls } from "framer-motion";
import React from "react";

function ManualAnimation() {
  const controls = useAnimationControls(); // Khởi tạo controls

  const startAnimation = async () => {
    await controls.start({ x: 100, rotate: 360 }); // Chạy animation 1
    await controls.start({ x: 0, rotate: 0 });     // Chạy animation 2
  };

  return (
    <div>
      <motion.div
        animate={controls} // Gắn controls vào component
        style={{
          width: 50,
          height: 50,
          backgroundColor: '#e74c3c',
          borderRadius: '10px',
          margin: '20px 0'
        }}
      />
      <button onClick={startAnimation}>
        Start Animation
      </button>
    </div>
  );
}

Giải thích:

  • useAnimationControls(): Hook này trả về một object controls mà bạn có thể sử dụng để bắt đầu hoặc dừng animation.
  • Chúng ta gắn object controls này vào component motion.div thông qua prop animate={controls}. Điều này báo cho Framer Motion biết rằng animation của component này sẽ được điều khiển bởi object controls.
  • Hàm startAnimation là một hàm async sử dụng controls.start(). controls.start() có thể nhận một object style/transform (giống như prop animate) hoặc tên của một variant. Nó trả về một Promise, cho phép chúng ta đợi animation hiện tại kết thúc trước khi bắt đầu animation tiếp theo (ví dụ: chạy đến x=100, sau đó mới quay về x=0).
  • Nút bấm gọi hàm startAnimation khi được click, kích hoạt chuỗi animation.
  • TypeScript giúp đảm bảo các thuộc tính bạn truyền vào controls.start() là hợp lệ.

Các hook khác như useScroll giúp tạo animation dựa trên vị trí cuộn trang, useTransform giúp liên kết giá trị của một animation (ví dụ: vị trí cuộn) với giá trị của một thuộc tính khác (ví dụ: opacity hoặc scale).

Tips và Best Practices
  • Sử dụng AnimatePresence: Khi component của bạn được mount/unmount (thêm/bớt khỏi DOM), hãy bọc chúng trong AnimatePresence để tạo hiệu ứng fade-in/fade-out hoặc các hiệu ứng vào/ra tùy chỉnh.
  • Hiệu suất: Đối với các animation phức tạp hoặc trên nhiều phần tử, hãy chú ý đến hiệu suất. Sử dụng các thuộc tính transform (x, y, scale, rotate) thường hiệu quả hơn việc animate các thuộc tính layout như width, height, margin, padding.
  • Framer Motion DevTools: Cài đặt extension Framer Motion DevTools cho trình duyệt để kiểm tra và debug animation dễ dàng hơn.
  • Kết hợp với CSS/Styled-components: Bạn hoàn toàn có thể sử dụng Framer Motion kết hợp với CSS Modules, Styled Components, hoặc Tailwind CSS. Framer Motion chỉ điều khiển các thuộc tính style động mà bạn chỉ định.

Framer Motion là một công cụ tuyệt vời để thêm sự sống động và tương tác vào ứng dụng React của bạn. Khi kết hợp với TypeScript, bạn không chỉ tạo ra các animation mượt mà mà còn phát triển code một cách an toànhiệu quả hơn. Hãy khám phá các tài liệu của Framer Motion để tìm hiểu sâu hơn về tất cả các tính năng mạnh mẽ mà nó cung cấp!

Comments

There are no comments at the moment.