Офлайн-режим
Мобильные приложения часто работают без интернета. Нужно это учитывать.
NetInfo
Определяет состояние сети:
npx expo install @react-native-community/netinfo
import NetInfo from '@react-native-community/netinfo';
const unsubscribe = NetInfo.addEventListener((state) => {
console.log('Connected:', state.isConnected);
console.log('Type:', state.type); // 'wifi' | 'cellular' | 'none' | ...
});
return () => unsubscribe();
Хук онлайн-статуса
function useOnlineStatus() {
const [online, setOnline] = useState(true);
useEffect(() => {
const unsub = NetInfo.addEventListener((state) => {
setOnline(!!state.isConnected);
});
return () => unsub();
}, []);
return online;
}
Индикатор офлайн
function Screen() {
const online = useOnlineStatus();
return (
<View style={{ flex: 1 }}>
{!online && (
<View style={styles.offlineBanner}>
<Text>Нет подключения к интернету</Text>
</View>
)}
{/* контент */}
</View>
);
}
TanStack Query + офлайн
import { onlineManager } from '@tanstack/react-query';
onlineManager.setEventListener((setOnline) => {
return NetInfo.addEventListener((state) => {
setOnline(!!state.isConnected);
});
});
Запросы не выполняются, пока нет сети. При появлении — выполняются автоматически.
Кеш для офлайн
Persist Query Client сохраняет данные между запусками:
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister, maxAge: 24 * 60 * 60 * 1000 }}
>
<App />
</PersistQueryClientProvider>
Пользователь открывает приложение офлайн — данные из кеша показываются сразу.
Мутации в офлайн
Очередь мутаций:
useMutation({
mutationFn: createPost,
onMutate: async (data) => {
// оптимистично добавить
},
});
TanStack Query сохраняет мутации и повторяет при появлении сети (с persistQueryClient + defaultOptions.mutations).
AsyncStorage для черновиков
function useDraft(key: string) {
const [draft, setDraft] = useState('');
useEffect(() => {
AsyncStorage.getItem(`draft:${key}`).then((v) => v && setDraft(v));
}, [key]);
useEffect(() => {
const timeout = setTimeout(() => {
AsyncStorage.setItem(`draft:${key}`, draft);
}, 500);
return () => clearTimeout(timeout);
}, [key, draft]);
return [draft, setDraft] as const;
}
Синхронизация
Паттерн: локальные изменения → очередь → отправка при появлении сети.
type PendingChange = {
id: string;
type: 'create' | 'update' | 'delete';
endpoint: string;
data: unknown;
};
async function queueChange(change: PendingChange) {
const queue = JSON.parse(await AsyncStorage.getItem('pending') ?? '[]');
queue.push(change);
await AsyncStorage.setItem('pending', JSON.stringify(queue));
}
async function flushQueue() {
const queue: PendingChange[] = JSON.parse(await AsyncStorage.getItem('pending') ?? '[]');
const remaining = [];
for (const change of queue) {
try {
await api.request({
method: change.type === 'delete' ? 'delete' : change.type === 'create' ? 'post' : 'put',
url: change.endpoint,
data: change.data,
});
} catch {
remaining.push(change);
}
}
await AsyncStorage.setItem('pending', JSON.stringify(remaining));
}
Оптимистичные обновления офлайн
const mutation = useMutation({
mutationFn: (data: PostInput) => api.post('/posts', data),
onMutate: async (data) => {
await queryClient.cancelQueries({ queryKey: ['posts'] });
const previous = queryClient.getQueryData(['posts']);
const optimistic = { ...data, id: Date.now() };
queryClient.setQueryData(['posts'], (old: Post[]) => [...old, optimistic]);
return { previous };
},
onError: (_, __, context) => {
queryClient.setQueryData(['posts'], context?.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
Пользователь видит изменения сразу, даже офлайн.
Итоги
NetInfoдля статуса сетиonlineManagerдля TanStack Query- Persist для кеша
- Черновики в AsyncStorage
- Очередь мутаций для синхронизации
- Оптимистичные обновления