Bạn kéo danh sách để tải lại, spinner vừa xuất hiện đã tắt hoặc lại quay mãi sau một lỗi. RefreshControl không tự biết Promise tải dữ liệu đã hoàn tất. Nó hiển thị theo prop refreshing do component của bạn quản lý.
Bài đối chiếu React Native 0.88 ngày 30/10/2026. Ví dụ dùng JavaScript/JSX, dữ liệu giả trong bộ nhớ và một độ trễ minh họa; không gọi server. Phần điều phối Promise được kiểm tra riêng trên Node.js 24.18.1, còn JSX đã rà API nhưng chưa build hoặc thử thao tác kéo trên Android/iOS.
Chốt hợp đồng của refreshing
RefreshControl reference ghi refreshing là controlled prop. Khi bắt đầu xử lý onRefresh, cần cập nhật nó thành true; khi công việc kết thúc, đưa nó về false. Đừng giả định khai báo async cho onRefresh khiến component native tự theo dõi Promise.
State phục vụ render, còn ref có thể giữ một cờ thay đổi ngay trong handler mà không cần chờ lần render mới. Trong ví dụ, busy ngăn lượt refresh thứ hai trong khi lượt đầu còn chạy. Nó không phải khóa ở server hoặc cơ chế bảo đảm idempotency nghiệp vụ.
Tạo một lượt tải giả có cả thành công và lỗi
async function loadDemo(shouldFail) {
await new Promise(resolve => setTimeout(resolve, 500));
if (shouldFail) throw new Error('Lỗi giả lập');
return [{id: 'demo', title: 'Dữ liệu đã được tải lại'}];
}
500 là độ trễ được đặt để dễ quan sát demo, không phải số đo mạng. Hàm luôn trả cùng một bộ dữ liệu nhỏ khi thành công. Khi đưa vào app thật, thay bằng hàm tải của dự án và giữ chính sách lỗi rõ ràng.
Đặt cleanup vào finally
Đặt loadDemo ở trên trong cùng file với component dưới đây. Nút đầu màn hình đổi chế độ lỗi cho lần kéo tiếp theo, giúp kiểm tra hai nhánh mà không cần làm hỏng endpoint thật.
import React, {useEffect, useRef, useState} from 'react';
import {Button, FlatList, RefreshControl, Text, View} from 'react-native';
export default function RefreshDemo() {
const [items, setItems] = useState([]);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState('');
const [shouldFail, setShouldFail] = useState(false);
const busy = useRef(false);
const active = useRef(true);
useEffect(() => {
active.current = true;
return () => { active.current = false; };
}, []);
async function onRefresh() {
if (busy.current || !active.current) return;
busy.current = true;
setRefreshing(true);
setError('');
try {
const next = await loadDemo(shouldFail);
if (active.current) setItems(next);
} catch (_) {
if (active.current) setError('Tải lại chưa thành công. Bạn có thể thử lại.');
} finally {
busy.current = false;
if (active.current) setRefreshing(false);
}
}
return (
<View style={{flex: 1, padding: 16}}>
<Button
title={shouldFail ? 'Đang mô phỏng lỗi: tắt' : 'Bật mô phỏng lỗi'}
onPress={() => setShouldFail(value => !value)}
/>
{error !== '' && <Text accessibilityLiveRegion="polite">{error}</Text>}
<FlatList
style={{flex: 1}}
contentContainerStyle={{flexGrow: 1, paddingVertical: 16}}
data={items}
keyExtractor={item => item.id}
renderItem={({item}) => <Text>{item.title}</Text>}
ListEmptyComponent={<Text>Chưa có dữ liệu. Kéo xuống để tải thử.</Text>}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
/>
</View>
);
}
Finally chạy sau cả nhánh thành công lẫn lỗi, nên cờ busy được mở lại. Nếu component đã unmount, code không cập nhật state của nó; điều này chỉ bỏ qua kết quả muộn, không hủy Timer hoặc request đang chạy.
useRef giải thích ref phù hợp cho giá trị không dùng trực tiếp để render. useEffect mô tả cleanup khi component được gỡ và vòng kiểm tra bổ sung trong development. Đừng dùng cờ active làm bằng chứng đã cancel công việc bên dưới.
Kiểm tra thành công, lỗi và lượt chồng nhau
Ở chế độ thành công, kéo từ đầu danh sách: kiểm tra indicator được điều khiển trong thời gian chờ và dữ liệu xuất hiện sau khi hoàn tất. Bật mô phỏng lỗi rồi kéo lại: dữ liệu cũ cần được giữ, lỗi hiện bằng text và lượt sau vẫn có thể bắt đầu.
Thử gọi handler hai lần liên tiếp trong một test điều phối: lượt thứ hai phải không gọi loadDemo trong khi busy còn true. Kiểm tra cả trường hợp loader reject để bảo đảm busy được mở trong finally. Đây là phép kiểm tra logic khác với thao tác gesture trên native view.
Với danh sách rỗng, thử trực tiếp trên cả Android và iOS. Scroll container cần chiều cao bị giới hạn từ cây cha; flex: 1 phải truyền đúng qua layout của màn hình. FlatList reference là nguồn đối chiếu cho list và refresh props, nhưng demo chưa xác nhận hành vi của mọi cấu hình lồng scroll.
Giới hạn khi nối API thật
Không dùng cùng refreshing cho mọi loại tải nếu màn hình còn initial load hoặc tải trang tiếp theo với UI khác. Xác định dữ liệu nào được thay, dữ liệu cũ có giữ khi lỗi không, và chính sách xử lý request không bao giờ kết thúc. Loader thật cần timeout/hủy theo hợp đồng riêng để finally có cơ hội được đi tới khi Promise settle.
Thông báo live region trong ví dụ dành cho Android; thử cách đọc lỗi trên iOS thay vì giả định cùng một prop bảo đảm giống nhau. Khi app có nhiều nguồn cập nhật danh sách, còn cần giải quyết thứ tự kết quả, không chỉ khóa một nút kéo.
RefreshControl chỉ cần một hợp đồng rõ ràng: bắt đầu lượt thì bật state, kết thúc lượt thì tắt, lỗi không làm mất khả năng thử lại. Giữ logic đó tách khỏi gesture giúp bạn kiểm tra và thay loader dễ hơn.




