React Query
Gestion du server state avec TanStack Query : caching, refetching et optimistic updates.
Overview
React Query (TanStack Query) est utilisé pour gérer l'état serveur dans l'application. Il fournit le caching automatique, le refetching intelligent et les mises à jour optimistes.
Caching
Cache automatique
Refetch
Actualisation smart
Fast
Optimistic updates
Stale
Gestion fraîcheur
Configuration
Configuration du QueryClient avec les options par défaut.
src/lib/queryClient.tstypescript
import { QueryClient } from '@tanstack/react-query';
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import AsyncStorage from '@react-native-async-storage/async-storage';
// Create QueryClient with default options
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Data is considered fresh for 5 minutes
staleTime: 5 * 60 * 1000,
// Keep data in cache for 30 minutes
gcTime: 30 * 60 * 1000,
// Retry failed requests 3 times
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
// Refetch on window focus (mobile: app foreground)
refetchOnWindowFocus: true,
// Don't refetch on mount if data is fresh
refetchOnMount: true,
// Refetch on network reconnect
refetchOnReconnect: true,
// Network mode
networkMode: 'online',
},
mutations: {
// Retry mutations once
retry: 1,
networkMode: 'online',
},
},
});
// Persister for offline support
export const persister = createAsyncStoragePersister({
storage: AsyncStorage,
key: 'YANIPAY_QUERY_CACHE',
throttleTime: 1000,
});
// Queries to persist offline
export const persistedQueryKeys = [
'user',
'balance',
'wallets',
'cards',
'transactions',
];app/_layout.tsxtsx
import { QueryClientProvider, focusManager, onlineManager } from '@tanstack/react-query';
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import { AppState, Platform } from 'react-native';
import NetInfo from '@react-native-community/netinfo';
import { queryClient, persister } from '@/lib/queryClient';
// Setup focus manager for React Native
focusManager.setEventListener((handleFocus) => {
const subscription = AppState.addEventListener('change', (state) => {
handleFocus(state === 'active');
});
return () => {
subscription.remove();
};
});
// Setup online manager
onlineManager.setEventListener((setOnline) => {
return NetInfo.addEventListener((state) => {
setOnline(!!state.isConnected);
});
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister }}
onSuccess={() => {
// Resume mutations after hydration
queryClient.resumePausedMutations();
}}
>
{children}
</PersistQueryClientProvider>
);
}Query Keys
Organisation des clés de requête avec factory pattern.
src/lib/queryKeys.tstypescript
// Query key factory pattern for type safety and organization
export const queryKeys = {
// User queries
user: {
all: ['user'] as const,
profile: () => [...queryKeys.user.all, 'profile'] as const,
preferences: () => [...queryKeys.user.all, 'preferences'] as const,
devices: () => [...queryKeys.user.all, 'devices'] as const,
sessions: () => [...queryKeys.user.all, 'sessions'] as const,
},
// Wallet queries
wallet: {
all: ['wallet'] as const,
list: () => [...queryKeys.wallet.all, 'list'] as const,
detail: (currency: string) => [...queryKeys.wallet.all, 'detail', currency] as const,
balance: () => [...queryKeys.wallet.all, 'balance'] as const,
totalBalance: () => [...queryKeys.wallet.all, 'total'] as const,
},
// Transaction queries
transactions: {
all: ['transactions'] as const,
list: (filters?: TransactionFilters) =>
[...queryKeys.transactions.all, 'list', filters] as const,
detail: (id: string) => [...queryKeys.transactions.all, 'detail', id] as const,
recent: (limit: number) =>
[...queryKeys.transactions.all, 'recent', limit] as const,
statistics: (period: string) =>
[...queryKeys.transactions.all, 'stats', period] as const,
},
// Card queries
cards: {
all: ['cards'] as const,
list: () => [...queryKeys.cards.all, 'list'] as const,
detail: (id: string) => [...queryKeys.cards.all, 'detail', id] as const,
transactions: (cardId: string) =>
[...queryKeys.cards.all, 'transactions', cardId] as const,
},
// Payment queries
payments: {
all: ['payments'] as const,
scheduled: () => [...queryKeys.payments.all, 'scheduled'] as const,
upcoming: (days: number) => [...queryKeys.payments.all, 'upcoming', days] as const,
requests: (type: 'sent' | 'received') =>
[...queryKeys.payments.all, 'requests', type] as const,
},
// KYC queries
kyc: {
all: ['kyc'] as const,
status: () => [...queryKeys.kyc.all, 'status'] as const,
documents: () => [...queryKeys.kyc.all, 'documents'] as const,
},
// Crypto queries
crypto: {
all: ['crypto'] as const,
prices: (currencies?: string[]) =>
[...queryKeys.crypto.all, 'prices', currencies] as const,
price: (currency: string) =>
[...queryKeys.crypto.all, 'price', currency] as const,
history: (currency: string, interval: string) =>
[...queryKeys.crypto.all, 'history', currency, interval] as const,
},
} as const;
// Type helper for query keys
type QueryKeys = typeof queryKeys;Custom Hooks
Hooks personnalisés utilisant React Query.
src/hooks/useQueries.tstypescript
import { useQuery, useInfiniteQuery, UseQueryOptions } from '@tanstack/react-query';
import { queryKeys } from '@/lib/queryKeys';
import { walletService } from '@/services/walletService';
import { transactionService, TransactionFilters } from '@/services/transactionService';
// Balance query with auto-refetch
export function useBalance() {
return useQuery({
queryKey: queryKeys.wallet.balance(),
queryFn: walletService.getBalance,
staleTime: 10 * 1000, // 10 seconds
refetchInterval: 60 * 1000, // Refetch every minute
});
}
// Total portfolio balance
export function useTotalBalance() {
return useQuery({
queryKey: queryKeys.wallet.totalBalance(),
queryFn: walletService.getTotalBalance,
staleTime: 30 * 1000,
});
}
// Wallets list
export function useWallets() {
return useQuery({
queryKey: queryKeys.wallet.list(),
queryFn: walletService.getWallets,
staleTime: 5 * 60 * 1000,
});
}
// Single wallet
export function useWallet(currency: string) {
return useQuery({
queryKey: queryKeys.wallet.detail(currency),
queryFn: () => walletService.getWallet(currency),
enabled: !!currency,
});
}
// Recent transactions
export function useRecentTransactions(limit = 5) {
return useQuery({
queryKey: queryKeys.transactions.recent(limit),
queryFn: () => transactionService.getRecentTransactions(limit),
staleTime: 30 * 1000,
});
}
// Infinite transactions list
export function useInfiniteTransactions(filters?: TransactionFilters) {
return useInfiniteQuery({
queryKey: queryKeys.transactions.list(filters),
queryFn: ({ pageParam = 1 }) =>
transactionService.getTransactions(filters, pageParam, 20),
getNextPageParam: (lastPage) =>
lastPage.pagination.hasMore ? lastPage.pagination.page + 1 : undefined,
initialPageParam: 1,
});
}
// Transaction statistics
export function useTransactionStats(period: 'week' | 'month' | 'year') {
return useQuery({
queryKey: queryKeys.transactions.statistics(period),
queryFn: () => transactionService.getStatistics(period),
staleTime: 5 * 60 * 1000,
});
}
// Generic query hook factory
export function createQueryHook<TData, TParams extends readonly unknown[]>(
queryKeyFn: (...params: TParams) => readonly unknown[],
queryFn: (...params: TParams) => Promise<TData>,
options?: Omit<UseQueryOptions<TData>, 'queryKey' | 'queryFn'>
) {
return (...params: TParams) => {
return useQuery({
queryKey: queryKeyFn(...params),
queryFn: () => queryFn(...params),
...options,
});
};
}Mutations
Mutations avec gestion des erreurs et invalidation du cache.
src/hooks/useMutations.tstypescript
import { useMutation, useQueryClient, MutationOptions } from '@tanstack/react-query';
import { queryKeys } from '@/lib/queryKeys';
import { transferService, SendMoneyDTO } from '@/services/transferService';
import { cardPaymentService } from '@/services/cardPaymentService';
import { Alert } from 'react-native';
// Send money mutation
export function useSendMoney() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: SendMoneyDTO) => transferService.sendMoney(data),
onSuccess: (result) => {
// Invalidate affected queries
queryClient.invalidateQueries({ queryKey: queryKeys.wallet.balance() });
queryClient.invalidateQueries({ queryKey: queryKeys.transactions.all });
// Show success message
Alert.alert(
'Paiement envoyé',
`${result.amount} ${result.currency} envoyé à ${result.recipient.name}`
);
},
onError: (error: Error) => {
Alert.alert('Erreur', error.message);
},
});
}
// Toggle card freeze
export function useToggleCardFreeze() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: cardPaymentService.toggleFreeze,
onSuccess: (updatedCard) => {
// Update cache directly
queryClient.setQueryData(
queryKeys.cards.detail(updatedCard.id),
updatedCard
);
// Invalidate cards list
queryClient.invalidateQueries({ queryKey: queryKeys.cards.list() });
},
});
}
// Order card mutation
export function useOrderCard() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: cardPaymentService.orderCard,
onSuccess: (newCard) => {
// Add to cache
queryClient.setQueryData(
queryKeys.cards.list(),
(old: Card[] | undefined) => old ? [...old, newCard] : [newCard]
);
},
});
}
// Create scheduled payment
export function useCreateScheduledPayment() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: scheduledPaymentService.createScheduledPayment,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.payments.scheduled() });
queryClient.invalidateQueries({ queryKey: queryKeys.payments.upcoming(30) });
},
});
}
// Generic mutation hook factory
export function createMutationHook<TData, TVariables, TError = Error>(
mutationFn: (variables: TVariables) => Promise<TData>,
options?: Omit<MutationOptions<TData, TError, TVariables>, 'mutationFn'>
) {
const queryClient = useQueryClient();
return () =>
useMutation({
mutationFn,
...options,
});
}Optimistic Updates
Mises à jour optimistes pour une UX instantanée.
Optimistic Update Exampletypescript
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { queryKeys } from '@/lib/queryKeys';
// Send money with optimistic update
export function useSendMoneyOptimistic() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: transferService.sendMoney,
// Called before mutation
onMutate: async (newTransfer) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: queryKeys.wallet.balance() });
await queryClient.cancelQueries({ queryKey: queryKeys.transactions.recent(5) });
// Snapshot previous values
const previousBalance = queryClient.getQueryData(queryKeys.wallet.balance());
const previousTransactions = queryClient.getQueryData(
queryKeys.transactions.recent(5)
);
// Optimistically update balance
queryClient.setQueryData(queryKeys.wallet.balance(), (old: WalletBalance) => ({
...old,
available: old.available - newTransfer.amount,
pending: old.pending + newTransfer.amount,
}));
// Optimistically add transaction
const optimisticTransaction = {
id: `temp-${Date.now()}`,
type: 'SEND',
status: 'PENDING',
amount: newTransfer.amount,
currency: newTransfer.currency,
createdAt: new Date().toISOString(),
counterparty: { name: 'Processing...' },
};
queryClient.setQueryData(
queryKeys.transactions.recent(5),
(old: Transaction[]) => [optimisticTransaction, ...old.slice(0, 4)]
);
// Return context for rollback
return { previousBalance, previousTransactions };
},
// Rollback on error
onError: (err, newTransfer, context) => {
if (context?.previousBalance) {
queryClient.setQueryData(
queryKeys.wallet.balance(),
context.previousBalance
);
}
if (context?.previousTransactions) {
queryClient.setQueryData(
queryKeys.transactions.recent(5),
context.previousTransactions
);
}
},
// Refetch after mutation (success or error)
onSettled: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.wallet.balance() });
queryClient.invalidateQueries({ queryKey: queryKeys.transactions.all });
},
});
}
// Like/unlike with optimistic update
export function useLikeTransaction(transactionId: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => transactionService.toggleLike(transactionId),
onMutate: async () => {
await queryClient.cancelQueries({
queryKey: queryKeys.transactions.detail(transactionId),
});
const previous = queryClient.getQueryData(
queryKeys.transactions.detail(transactionId)
);
queryClient.setQueryData(
queryKeys.transactions.detail(transactionId),
(old: Transaction) => ({
...old,
isLiked: !old.isLiked,
likesCount: old.isLiked ? old.likesCount - 1 : old.likesCount + 1,
})
);
return { previous };
},
onError: (err, variables, context) => {
if (context?.previous) {
queryClient.setQueryData(
queryKeys.transactions.detail(transactionId),
context.previous
);
}
},
});
}