Bài 26.2: React Transition Group với TypeScript

Bài 26.2: React Transition Group với TypeScript
Chào mừng các bạn quay trở lại với chuỗi bài viết về lập trình Front-end! Hôm nay, chúng ta sẽ cùng nhau khám phá một thư viện cực kỳ hữu ích để mang lại sự sống động cho ứng dụng React của bạn: React Transition Group (RTG). Kết hợp nó với sức mạnh của TypeScript sẽ giúp chúng ta tạo ra các hiệu ứng chuyển động mượt mà, có thể bảo trì và ít lỗi hơn.
Tại sao cần React Transition Group?
React rất giỏi trong việc quản lý trạng thái và cập nhật DOM một cách hiệu quả. Tuy nhiên, khi một component được mount (xuất hiện) hoặc unmount (biến mất) khỏi DOM, React không tự động cung cấp các hook hoặc cơ chế để bạn dễ dàng thêm hiệu ứng chuyển động (animations hoặc transitions) trong quá trình đó. Component chỉ đơn giản là "có mặt" hoặc "biến mất".
Đây chính là lúc React Transition Group tỏa sáng. Thư viện này cung cấp một tập hợp các components giúp bạn quản lý các trạng thái của component trong quá trình mount/unmount, từ đó cho phép bạn áp dụng các CSS class hoặc chạy các hàm JavaScript vào đúng thời điểm để tạo ra hiệu ứng.
RTG không tự thực hiện animation cho bạn. Nó đóng vai trò là một bộ điều phối, giúp bạn dễ dàng tích hợp animations/transitions do bạn tự định nghĩa bằng CSS hoặc JavaScript.
Các Thành Phần Chính của React Transition Group
React Transition Group cung cấp một vài components chính, nhưng hai trong số đó là phổ biến nhất khi làm việc với CSS transitions:
<TransitionGroup>
: Component này không tự tạo hiệu ứng, mà nó theo dõi các con của nó. Khi một con được thêm hoặc bớt, nó sẽ thông báo cho component con đó (thường là<CSSTransition>
) biết để bắt đầu quá trình chuyển trạng thái. Nó đặc biệt hữu ích khi bạn muốn animates các phần tử trong một danh sách (list) khi chúng được thêm hoặc xóa.<CSSTransition>
: Đây là component mà bạn sẽ sử dụng để bọc quanh một component duy nhất mà bạn muốn tạo hiệu ứng. Nó quản lý việc thêm/bớt các CSS classes vào component con dựa trên trạng thái chuyển động (entering, entered, exiting, exited).
Trong bài này, chúng ta sẽ tập trung vào cách sử dụng <CSSTransition>
(thường kết hợp bên trong <TransitionGroup>
cho danh sách) và cách kết hợp nó với TypeScript.
Bắt Đầu với <CSSTransition>
Hãy xem một ví dụ đơn giản: làm mờ dần (fade) một khối div
khi nó xuất hiện hoặc biến mất.
Đầu tiên, cài đặt thư viện:
npm install react-transition-group @types/react-transition-group
# hoặc
yarn add react-transition-group @types/react-transition-group
Bây giờ, hãy tạo một component đơn giản:
// FadeBox.tsx
import React, { useState } from 'react';
import { CSSTransition } from 'react-transition-group';
import './FadeBox.css'; // Chúng ta sẽ tạo file CSS này sau
const FadeBox: React.FC = () => {
const [isVisible, setIsVisible] = useState(false);
const nodeRef = React.useRef(null); // Cần ref cho CSSTransition >= v4
return (
<div>
<button onClick={() => setIsVisible(!isVisible)}>
{isVisible ? 'Ẩn Box' : 'Hiện Box'}
</button>
<CSSTransition
in={isVisible} // Prop 'in' quyết định trạng thái: true -> entering/entered, false -> exiting/exited
timeout={300} // Thời gian của transition tính bằng ms
classNames="fade" // Tiền tố cho các CSS class
unmountOnExit // Khi transition thoát kết thúc, loại bỏ component khỏi DOM
nodeRef={nodeRef} // Kết nối ref với component con
>
{/* Component con được áp dụng transition */}
<div ref={nodeRef} className="fade-box">
Đây là Box cần Transition!
</div>
</CSSTransition>
</div>
);
};
export default FadeBox;
Giải thích Code:
- Chúng ta sử dụng hook
useState
để quản lý trạng thái hiển thị (isVisible
). - Button dùng để bật/tắt trạng thái
isVisible
. CSSTransition
bọc lấydiv
mà chúng ta muốn tạo hiệu ứng.- Prop
in={isVisible}
: Đây là prop quan trọng nhất. Nó báo choCSSTransition
biết khi nào thì component con nên được coi là "vào" (true
) hay "ra" (false
). - Prop
timeout={300}
: Chỉ định thời gian tối đa của transition. RTG sẽ đợi khoảng thời gian này trước khi chuyển sang trạng thái-done
hoặc xóa component nếu cóunmountOnExit
. - Prop
classNames="fade"
: Đây là tiền tố mà RTG sẽ sử dụng để tạo ra các CSS class trong quá trình chuyển động. - Prop
unmountOnExit
: Nếutrue
, sau khi transition thoát (exiting
/exited
) hoàn thành, component con sẽ bị gỡ bỏ hoàn toàn khỏi DOM. Điều này rất hữu ích cho các thành phần như modal, tooltip, notification... - Prop
nodeRef={nodeRef}
: Bắt buộc từ RTG v4 trở lên để CSSTransition biết chính xác phần tử DOM nào cần áp dụng classes (thay vì dựa vàofindDOMNode
không được khuyến khích). Chúng ta tạo mộtuseRef
và gán nó vào cảCSSTransition
và phần tử DOM bên trong.
Bây giờ, chúng ta cần định nghĩa các CSS classes trong file FadeBox.css
:
/* FadeBox.css */
/* Trạng thái ban đầu khi chuẩn bị enter */
.fade-enter {
opacity: 0; /* Bắt đầu từ mờ */
}
/* Trạng thái active khi đang enter */
.fade-enter-active {
opacity: 1; /* Chuyển đến rõ */
transition: opacity 300ms ease-in; /* Áp dụng transition */
}
/* Trạng thái sau khi enter hoàn thành */
.fade-enter-done {
opacity: 1; /* Ở trạng thái rõ */
}
/* Trạng thái ban đầu khi chuẩn bị exit */
.fade-exit {
opacity: 1; /* Bắt đầu từ rõ */
}
/* Trạng thái active khi đang exit */
.fade-exit-active {
opacity: 0; /* Chuyển đến mờ */
transition: opacity 300ms ease-out; /* Áp dụng transition */
}
/* Trạng thái sau khi exit hoàn thành */
.fade-exit-done {
opacity: 0; /* Ở trạng thái mờ (và sẽ bị xóa khỏi DOM nếu có unmountOnExit) */
}
/* Định kiểu cơ bản cho box */
.fade-box {
padding: 20px;
margin-top: 10px;
border: 1px solid #ccc;
background-color: #f0f0f0;
}
Giải thích CSS:
- Khi
in
chuyển từfalse
sangtrue
(entering):- RTG thêm class
fade-enter
. Bạn định nghĩa kiểu CSS cho trạng thái ban đầu của transition (ví dụ:opacity: 0
). - RTG thêm class
fade-enter-active
(và xóafade-enter
). Đây là lúc bạn định nghĩa trạng thái kết thúc của transition và quan trọng nhất là thuộc tínhtransition
để chỉ ra thuộc tính CSS nào sẽ thay đổi và trong bao lâu. - Sau khi transition hoàn thành (hoặc hết
timeout
), RTG thêm classfade-enter-done
(và xóafade-enter-active
). Đây là trạng thái cuối cùng sau khi enter.
- RTG thêm class
- Khi
in
chuyển từtrue
sangfalse
(exiting):- RTG thêm class
fade-exit
. Bạn định nghĩa kiểu CSS cho trạng thái ban đầu của transition (ví dụ:opacity: 1
- trạng thái trước khi bắt đầu thoát). - RTG thêm class
fade-exit-active
(và xóafade-exit
). Đây là lúc bạn định nghĩa trạng thái kết thúc của transition thoát và áp dụng lại thuộc tínhtransition
. - Sau khi transition thoát hoàn thành (hoặc hết
timeout
), RTG thêm classfade-exit-done
(và xóafade-exit-active
). Đây là trạng thái cuối cùng sau khi exit. Nếu cóunmountOnExit
, component sẽ bị xóa khỏi DOM sau khi class này được thêm.
- RTG thêm class
Với sự kết hợp này, khi bạn nhấn nút, div
sẽ xuất hiện hoặc biến mất một cách mượt mà thay vì đột ngột.
Kết Hợp TypeScript với <CSSTransition>
Trong ví dụ trên, chúng ta đã sử dụng TypeScript cho component React (FadeBox: React.FC
). Các props của CSSTransition
(in
, timeout
, classNames
, unmountOnExit
, nodeRef
) đều có sẵn kiểu định nghĩa trong @types/react-transition-group
, nên TypeScript sẽ tự động kiểm tra kiểu cho bạn khi bạn sử dụng chúng.
Ví dụ, nếu bạn gõ sai tên prop hoặc truyền sai kiểu dữ liệu (ví dụ: truyền string vào timeout
), TypeScript sẽ báo lỗi ngay lập tức, giúp bạn tránh các lỗi phổ biến trong quá trình phát triển.
// Ví dụ lỗi TypeScript (chỉ để minh họa)
<CSSTransition
in={isVisible}
timeout="300ms" // <- Lỗi TypeScript: Type 'string' is not assignable to type 'number | { enter?: number; exit?: number; appear?: number; }'.
// ... các props khác
>
// ...
</CSSTransition>
Sức mạnh của TypeScript ở đây là nó đảm bảo bạn đang sử dụng thư viện đúng cách theo định nghĩa của nó.
Bắt Đầu với <TransitionGroup>
và List Animations
Bây giờ, hãy nâng cấp ví dụ để animate các mục trong một danh sách khi chúng được thêm hoặc bớt. Đây là trường hợp sử dụng thường xuyên của TransitionGroup
.
// TodoList.tsx
import React, { useState, useRef } from 'react';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import { v4 as uuidv4 } from 'uuid'; // Cần cài đặt uuid để tạo key duy nhất
import './TodoList.css'; // File CSS cho list items
// Kiểu dữ liệu cho một mục công việc
interface TodoItem {
id: string;
text: string;
}
const TodoList: React.FC = () => {
const [todos, setTodos] = useState<TodoItem[]>([
{ id: uuidv4(), text: 'Học React Transition Group' },
{ id: uuidv4(), text: 'Kết hợp với TypeScript' },
]);
const [newTodoText, setNewTodoText] = useState('');
// Ref cho TransitionGroup (cũng cần từ v4 trở lên)
const listRef = useRef<HTMLUListElement>(null);
const addTodo = () => {
if (newTodoText.trim() === '') return;
const newTodo: TodoItem = { id: uuidv4(), text: newTodoText };
setTodos([...todos, newTodo]);
setNewTodoText('');
};
const removeTodo = (id: string) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div>
<h2>Danh sách Công việc</h2>
<div>
<input
type="text"
value={newTodoText}
onChange={(e) => setNewTodoText(e.target.value)}
placeholder="Thêm công việc mới..."
/>
<button onClick={addTodo}>Thêm</button>
</div>
{/* Bọc danh sách bằng TransitionGroup */}
<TransitionGroup component="ul" className="todo-list" nodeRef={listRef}>
{todos.map((todo) => (
// Mỗi mục trong danh sách được bọc bằng CSSTransition
<CSSTransition
key={todo.id} // KEY là BẮT BUỘC và PHẢI DUY NHẤT
timeout={300}
classNames="todo-item-transition" // Tiền tố CSS khác
unmountOnExit // Xóa khỏi DOM sau khi thoát
nodeRef={React.createRef<HTMLLIElement>()} // Mỗi CSSTransition cần ref riêng
>
{/* Component con (li) */}
<li className="todo-item">
{todo.text}
<button onClick={() => removeTodo(todo.id)}>Xóa</button>
</li>
</CSSTransition>
))}
</TransitionGroup>
</div>
);
};
export default TodoList;
Giải thích Code:
- Chúng ta có một state
todos
là một mảng cácTodoItem
. Kiểu dữ liệuTodoItem
được định nghĩa rõ ràng bằng TypeScript, bao gồmid
vàtext
.useState<TodoItem[]>
đảm bảotodos
luôn là một mảng các đối tượng có cấu trúc này. - Các hàm
addTodo
vàremoveTodo
cập nhật statetodos
theo cách thông thường của React. TransitionGroup
được sử dụng để bọc thẻ<ul>
. Propcomponent="ul"
chỉ địnhTransitionGroup
sẽ render ra thẻ<ul>
trong DOM. Nó cũng cần mộtnodeRef
từ v4 trở lên.- Bên trong
TransitionGroup
, chúng ta dùngmap
để render từngtodo
thành một<li>
. - Quan trọng: Mỗi
<li>
(hoặc component con trong danh sách) phải được bọc bởi mộtCSSTransition
. - Prop
key={todo.id}
: Đây là cực kỳ quan trọng. React sử dụngkey
để xác định item nào đã thay đổi, thêm hoặc xóa.TransitionGroup
dựa vàokey
này để biết khi nào cần kích hoạt transition cho mộtCSSTransition
cụ thể. Key phải là duy nhất trong danh sách. CSSTransition
ở đây tương tự như ví dụ trước, sử dụngtimeout
,classNames
,unmountOnExit
.nodeRef={React.createRef<HTMLLIElement>()}
: Mỗi instance củaCSSTransition
trongmap
cần mộtref
riêng.React.createRef()
tạo ra một ref mới cho mỗi item trong lần render đầu tiên. Chúng ta cũng cung cấp kiểu dữ liệuHTMLLIElement
cho ref để TypeScript biết ref này sẽ gắn vào một thẻ<li>
.
Bây giờ, file TodoList.css
sẽ cần các lớp tương tự, nhưng với tiền tố todo-item-transition
:
/* TodoList.css */
/* Styles for the list and items */
.todo-list {
list-style: none;
padding: 0;
margin-top: 15px;
}
.todo-item {
padding: 10px;
border: 1px solid #eee;
margin-bottom: 5px;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fff;
transition: opacity 300ms ease-in-out, transform 300ms ease-in-out; /* Có thể animate nhiều thuộc tính */
}
.todo-item button {
margin-left: 10px;
background-color: #ff6666;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
border-radius: 3px;
}
/* CSS Classes for Transition */
/* Entering */
.todo-item-transition-enter {
opacity: 0;
transform: translateX(-20px); /* Thêm hiệu ứng slide từ trái */
}
.todo-item-transition-enter-active {
opacity: 1;
transform: translateX(0);
/* Transition property đã được định nghĩa ở .todo-item */
}
/* Exiting */
.todo-item-transition-exit {
opacity: 1;
transform: translateX(0);
}
.todo-item-transition-exit-active {
opacity: 0;
transform: translateX(20px); /* Thêm hiệu ứng slide sang phải */
/* Transition property đã được định nghĩa ở .todo-item */
}
/* Note: chúng ta không cần -enter-done và -exit-done nếu chỉ animate opacity/transform
và unmountOnExit = true. Các style cuối cùng được áp dụng bởi lớp CSS thông thường (.todo-item)
sau khi transition kết thúc và trước khi bị xóa.
*/
Giải thích CSS:
- Chúng ta định nghĩa các lớp CSS cơ bản cho
.todo-list
và.todo-item
. - Các lớp với tiền tố
todo-item-transition-
được sử dụng bởiCSSTransition
. todo-item-transition-enter
vàtodo-item-transition-enter-active
tạo hiệu ứng mờ dần và trượt vào từ trái.todo-item-transition-exit
vàtodo-item-transition-exit-active
tạo hiệu ứng mờ dần và trượt ra sang phải.- Lưu ý rằng thuộc tính
transition
được định nghĩa trên lớp.todo-item
chung. Điều này là ổn vì các lớp-active
sẽ được thêm/bớt, kích hoạt transition dựa trên thuộc tínhtransition
đã có sẵn. Bạn cũng có thể định nghĩatransition
trực tiếp trong các lớp-active
.
Với setup này, khi bạn thêm một công việc mới, nó sẽ mờ dần và trượt vào danh sách. Khi bạn xóa, nó sẽ mờ dần và trượt ra trước khi biến mất hoàn toàn khỏi DOM.
Kết Hợp TypeScript với <TransitionGroup>
Trong ví dụ TodoList
, TypeScript đóng vai trò quan trọng trong việc định nghĩa kiểu dữ liệu cho state (TodoItem[]
) và đảm bảo rằng chúng ta truyền đúng kiểu dữ liệu khi tạo đối tượng TodoItem
mới.
// Khi thêm mới:
const newTodo: TodoItem = { id: uuidv4(), text: newTodoText };
// TypeScript kiểm tra xem newTodo có đúng cấu trúc { id: string, text: string } không.
// Khi xóa:
setTodos(todos.filter(todo => todo.id !== id));
// TypeScript kiểm tra xem id có phải là string không.
Hơn nữa, khi sử dụng CSSTransition
bên trong map
, việc chỉ định nodeRef={React.createRef<HTMLLIElement>()}
với kiểu HTMLLIElement
giúp TypeScript xác nhận rằng ref này dự kiến sẽ được gắn vào một phần tử <li>
.
Sự kết hợp của RTG và TypeScript mang lại:
- Hiệu ứng mượt mà: RTG giúp bạn kiểm soát timing và trạng thái của components.
- Code rõ ràng, dễ bảo trì: Việc sử dụng CSS classes giúp tách biệt animation logic khỏi React component logic.
- Giảm lỗi: TypeScript bắt các lỗi liên quan đến kiểu dữ liệu và việc sử dụng props của RTG component ngay trong quá trình phát triển.
Các Prop Hữu Ích Khác của CSSTransition
Ngoài các prop chính đã nói, CSSTransition
còn có một số prop callback rất hữu ích mà bạn có thể sử dụng để chạy code JavaScript vào các thời điểm cụ thể của transition. Các callback này cũng có định nghĩa kiểu trong TypeScript:
onEnter?(node: HTMLElement, isAppearing: boolean): void;
onEntering?(node: HTMLElement, isAppearing: boolean): void;
onEntered?(node: HTMLElement, isAppearing: boolean): void;
onExit?(node: HTMLElement): void;
onExiting?(node: HTMLElement): void;
onExited?(node: HTMLElement): void;
Bạn có thể sử dụng chúng cho các mục đích như:
- Thêm/bớt các class CSS tùy chỉnh không theo quy tắc
classNames
. - Tính toán và thiết lập các style inline trước khi transition bắt đầu (
onEnter
,onExit
). - Kích hoạt các animation phức tạp hơn bằng JavaScript (
onEntering
,onExiting
). - Thực hiện các hành động sau khi transition hoàn thành (ví dụ: focus vào một phần tử sau khi modal xuất hiện hoàn toàn trong
onEntered
, dọn dẹp state trongonExited
).
Với TypeScript, các callback này được định nghĩa rõ ràng là nhận đối số node
(phần tử DOM) và isAppearing
(chỉ có ở enter/entering/entered, cho biết đây có phải lần mount đầu tiên không), giúp bạn biết chính xác mình có thể làm gì trong mỗi callback.
Lời khuyên khi sử dụng React Transition Group
- Sử dụng
key
đúng cách: Đặc biệt vớiTransitionGroup
và danh sách, việc cung cấpkey
duy nhất và ổn định là quan trọng nhất để RTG hoạt động chính xác. - CSS là bạn: Hầu hết các trường hợp sử dụng RTG đều hiệu quả nhất khi kết hợp với CSS transitions/animations. Hãy nắm vững cách các lớp CSS được thêm/bớt.
- Ref là cần thiết (từ v4): Luôn nhớ cung cấp
nodeRef
choCSSTransition
vàTransitionGroup
nếu bạn đang dùng phiên bản 4 trở lên. TypeScript giúp bạn nhắc nhở về điều này. - Cân nhắc
unmountOnExit
: Nó rất hữu ích cho các component tạm thời (modal, popup) để giữ cho DOM sạch sẽ khi chúng không hiển thị. - Đối với animations phức tạp hơn: Nếu bạn cần phối hợp nhiều animation, timeline phức tạp, hoặc animation dựa trên physics, bạn có thể cân nhắc các thư viện mạnh mẽ hơn như Framer Motion hoặc React Spring, tuy nhiên RTG là lựa chọn tuyệt vời cho các transitions mount/unmount dựa trên CSS.
Comments