Xây dựng trò chơi Snake trong React: Giải quyết đóng băng trạng thái với useRef và requestAnimationFrame
Trò chơi Snake tưởng đơn giản nhưng khi dựng bằng React lại gặp phải vấn đề đóng băng trạng thái do useState trong vòng lặp requestAnimationFrame. Bài viết trình bày cách sử dụng useRef để tránh đóng băng, điều khiển tốc độ dựa trên timestamp, xử lý gói tường (wall wrap) và hỗ trợ điều khiển cảm ứng.

Xây dựng trò chơi Snake trong React: Giải quyết đóng băng trạng thái với useRef và requestAnimationFrame
Trò chơi Snake có vẻ rất đơn giản: di chuyển, ăn thức ăn, dài ra rồi lặp lại. Tuy nhiên, khi xây dựng trò chơi này trong React, một vấn đề phổ biến dễ gặp là đóng băng giá trị trạng thái (stale closure) khi dùng useState trong vòng lặp requestAnimationFrame.
Bài viết dưới đây trình bày một cách tiếp cận hiệu quả để xây dựng Snake bằng React, giữ mọi trạng thái game có thể thay đổi trong useRef nhằm tránh stale closure, sử dụng điều khiển tốc độ dựa trên timestamp, có thêm tính năng gói tường (wall wrap) và hỗ trợ điều khiển cảm ứng cho thiết bị di động.
Vấn đề đóng băng trạng thái (Stale Closure) trong React
Khi sử dụng useState bên trong callback requestAnimationFrame (RAF), mỗi callback được tạo ra sẽ "đóng băng" (capture) giá trị state tại thời điểm đó. Ví dụ, nếu bạn dùng:
const [snake, setSnake] = useState([]);
và đọc biến snake trong callback RAF, bạn sẽ luôn thấy mảng trống ban đầu dù rắn đã được kéo dài sau nhiều lượt đi. Đó là vì callback RAF đã lấy snapshot giá trị lúc callback được tạo, không phản ánh cập nhật mới.
Giải pháp: dùng useRef để lưu trữ trạng thái mutable
Để tránh stale closure, tất cả dữ liệu game cần truy cập trong vòng lặp game (RAF) được lưu trong các ref mutable, ví dụ:
const snakeRef = useRef<Pt[]>([]);
const dirRef = useRef<Dir>("RIGHT");
const nextDirRef = useRef<Dir>("RIGHT");
const foodRef = useRef<Pt>({ x: 5, y: 5 });
const scoreRef = useRef(0);
const speedRef = useRef(BASE_SPEED);
const lastTickRef = useRef(0);
const statusRef = useRef<Status>("idle");
const rafRef = useRef<number>(0);
Mỗi ref.current luôn cho giá trị mới nhất, tránh đóng băng như useState. Còn useState chỉ được dùng cho những thứ cần trigger render lại giao diện như score hoặc status hiển thị.
Vòng lặp game với requestAnimationFrame và điều khiển tốc độ theo thời gian
Thay vì dùng setInterval, vòng lặp game sử dụng requestAnimationFrame kết hợp điều kiện thời gian so sánh timestamp để kiểm soát tốc độ chuyển động:
const tick = useCallback((ts: number) => {
if (statusRef.current !== "playing") return;
rafRef.current = requestAnimationFrame(tick);
if (ts - lastTickRef.current < speedRef.current) {
draw(); // chỉ vẽ mà không cập nhật trạng thái
return;
}
lastTickRef.current = ts;
// Xử lý cập nhật trạng thái game, di chuyển rắn, va chạm...
}, [draw, placeFood]);
Mỗi frame RAF chạy ~60fps, nhưng logic game chỉ cập nhật khi đủ thời gian (cách nhau speedRef.current ms), tách biệt tốc độ render và tốc độ game để giảm tải.
Xử lý gói tường (Wall Wrap)
Khi đầu rắn vượt khỏi biên, nó sẽ xuất hiện lại ở bên kia màn:
const next: Pt = {
x: (head.x + (dir === "RIGHT" ? 1 : dir === "LEFT" ? -1 : 0) + COLS) % COLS,
y: (head.y + (dir === "DOWN" ? 1 : dir === "UP" ? -1 : 0) + ROWS) % ROWS,
};
Cộng thêm COLS hoặc ROWS trước khi lấy modulo tránh lỗi kết quả âm do cách tính modulo của JavaScript.
Tăng tốc độ theo điểm số
Mỗi khi ăn 5 thức ăn, tốc độ di chuyển rắn được tăng (giảm interval):
const newScore = scoreRef.current + 10;
scoreRef.current = newScore;
setScore(newScore);
if ((newScore / 10) % 5 === 0) {
speedRef.current = Math.max(MIN_SPEED, speedRef.current - SPEED_STEP);
}
Điều này giúp trò chơi trở nên thử thách hơn dần theo thời gian.
Xử lý thay đổi hướng và tránh quay đầu gấp
Hai ref quản lý hướng hiện tại và hướng tiếp theo:
const onKey = (e: KeyboardEvent) => {
const d = KEY_DIR[e.key];
if (!d) return;
if (d !== OPPOSITE[dirRef.current]) nextDirRef.current = d; // cấm quay 180°
};
dirRef.current = nextDirRef.current; // cập nhật ở đầu mỗi tick
Cách này tránh việc nhấn phím nhanh hai lần gây rắn quay đầu mất game.
Hỗ trợ cảm ứng, điều khiển vuốt trên thiết bị di động
Xử lý bắt đầu và kết thúc touch để phát hiện vuốt theo trục ưu tiên:
const onTouchStart = (e: React.TouchEvent) => {
touchStartRef.current = { x: e.touches[0].clientX, y: e.touches[0].clientY };
};
const onTouchEnd = (e: React.TouchEvent) => {
const dx = e.changedTouches[0].clientX - touchStartRef.current.x;
const dy = e.changedTouches[0].clientY - touchStartRef.current.y;
if (Math.abs(dx) < 12 && Math.abs(dy) < 12) return; // loại bỏ tap
const d: Dir = Math.abs(dx) > Math.abs(dy)
? (dx > 0 ? "RIGHT" : "LEFT")
: (dy > 0 ? "DOWN" : "UP");
if (d !== OPPOSITE[dirRef.current]) nextDirRef.current = d;
};
Ngưỡng 12px lọc bỏ nhấn nhẹ, hướng vuốt theo chiều dài nhất.
Vẽ trò chơi trên canvas
Trò chơi được vẽ trên canvas 400×400px (20×20 ô, mỗi ô 20px):
- Nền tối, lưới ô vuông nhẹ nhàng.
- Thức ăn phát sáng bằng gradient tia sáng đỏ hồng.
- Rắn có đầu màu tím sáng, đuôi dần tối đi tạo hiệu ứng chuyển màu.
Màu sắc gradient được tính toán toán học, không cần mảng màu.
Lưu điểm cao lên localStorage
Sử dụng localStorage để lưu điểm cao nhất:
useEffect(() => {
const hs = parseInt(localStorage.getItem("snake-hs") ?? "0");
if (hs) setHighScore(hs);
}, []);
// Khi game over
const hs = parseInt(localStorage.getItem("snake-hs") ?? "0");
if (scoreRef.current > hs) {
localStorage.setItem("snake-hs", String(scoreRef.current));
setHighScore(scoreRef.current);
}
Ghi dữ liệu localStorage đồng bộ nhưng không gây render lại, setHighScore mới chịu trách nhiệm cập nhật giao diện.
Tổng kết: Giữ React state tối giản, tập trung logic game trong refs
Thiết kế chính của trò chơi là:
- React state chỉ giữ những thứ UI cần cập nhật hiển thị như điểm số, trạng thái game.
- Các trạng thái game Phức tạp, hay thay đổi nhanh được lưu trong
useRef, tránh stale closure, giảm ảnh hưởng vòng render React tới vòng lặp game. - Vòng lặp game dùng
requestAnimationFramevới kiểm soát theo timestamp cho trải nghiệm mượt mà và tăng tốc theo tiến trình.
Bạn có thể chơi thử phiên bản game này tại ultimatetools.io.
Đây là ví dụ điển hình cho việc áp dụng thành công React hooks để xử lý logic game thời gian thực, đồng thời tận dụng tốt canvas để dựng hình trực quan, phù hợp với các dự án web game nhỏ gọn và hiệu quả.
Bài viết liên quan

Phần mềm
Anthropic ra mắt Claude Opus 4.7: Nâng cấp mạnh mẽ cho lập trình nhưng vẫn thua Mythos Preview
16 tháng 4, 2026

Công nghệ
Qwen3.6-35B-A3B: Quyền năng Lập trình Agentic, Nay Đã Mở Cửa Cho Tất Cả
16 tháng 4, 2026

Công nghệ
Spotify thắng kiện 322 triệu USD từ nhóm pirate Anna's Archive nhưng đối mặt với bài toán thu hồi
16 tháng 4, 2026
