@alejo-dev/xavi-providers
v1.0.2
Published
Un proveedor de API cliente modular y fuertemente tipado para React Native con soporte completo para React Query v5, persistencia automática, detección de conectividad y modo offline.
Downloads
41
Readme
@alejo-dev/xavi-providers
Un proveedor de API cliente modular y fuertemente tipado para React Native con soporte completo para React Query v5, persistencia automática, detección de conectividad y modo offline.
🚀 Características
- ✅ React Query v5 con persistencia AsyncStorage
- ✅ Hooks REST completos (GET, POST, PUT, PATCH, DELETE, UPLOAD)
- ✅ Hooks GraphQL (Query, Mutation, Infinite Query)
- ✅ Detección automática de conectividad y estado del servidor
- ✅ Modo offline manual para debugging
- ✅ Ping/health checks configurables
- ✅ Manejo de errores normalizado
- ✅ TypeScript fuerte con generics
- ✅ Compatible con React Native
📦 Instalación
npm install @alejo-dev/xavi-providers
# o
yarn add @alejo-dev/xavi-providersDependencias requeridas
Asegúrate de tener instaladas estas dependencias peer:
{
"@react-native-async-storage/async-storage": "^2.1.2",
"@tanstack/react-query": "^5.75.5",
"@tanstack/react-query-persist-client": "^5.77.1",
"axios": "^1.9.0",
"react": ">=19.0.0",
"react-native": ">=0.79.2"
}🏗️ Configuración Básica
1. Configura el QueryClient
import { QueryClient } from '@tanstack/react-query';
import { createAsyncStoragePersistor } from '@tanstack/react-query-persist-client';
import AsyncStorage from '@react-native-async-storage/async-storage';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutos
gcTime: 1000 * 60 * 60, // 1 hora
},
},
});
const asyncStoragePersistor = createAsyncStoragePersistor({
storage: AsyncStorage,
key: 'REACT_QUERY_CACHE',
});2. Envuelve tu App con el Provider
import React from 'react';
import { ApiClientProvider } from '@alejo-dev/xavi-providers';
export default function App() {
return (
<ApiClientProvider
queryClient={queryClient}
config={{
pingEndpoint: '/api/health',
enableServerHealthCheck: true,
enableInitialPing: true,
}}
>
{/* Tu aplicación aquí */}
</ApiClientProvider>
);
}🔧 Configuración Avanzada
const config = {
// Endpoint para health checks
pingEndpoint: '/api/health',
pingMethod: 'GET',
pingHeaders: { Authorization: 'Bearer token' },
pingTimeoutMs: 5000,
// Reintentos y offline
offlineRetryIntervalMs: 30000,
maxConsecutiveFailuresBeforeOffline: 3,
offlineStatusCodes: [502, 503, 504],
// Validación de respuesta de ping
pingResponseValidator: (response) => response.status === 200,
// Modo offline manual
manualOfflineStrategy: 'block-network', // o 'serve-cache-and-block-network'
enableDebugOfflineControls: __DEV__,
// --- Health check mode ---
// 'always' (default): health check inicial y periódicos
// 'on-failure': solo hace health check cuando una request falla
healthCheckMode: 'always', // o 'on-failure'
};📡 Uso de Hooks REST
GET Query
import { useGetQuery } from '@alejo-dev/xavi-providers';
interface User {
id: number;
name: string;
email: string;
}
function UserProfile({ userId }: { userId: number }) {
const { data, isLoading, error } = useGetQuery<User>({
endpoint: `/api/users/${userId}`,
queryKey: ['user', userId],
enabled: !!userId,
});
if (isLoading) return <Text>Loading...</Text>;
if (error) return <Text>Error: {error.message}</Text>;
return (
<View>
<Text>{data?.name}</Text>
<Text>{data?.email}</Text>
</View>
);
}Infinite Query
import { useInfiniteGetQuery } from '@alejo-dev/xavi-providers';
function PostsList() {
const { data, fetchNextPage, hasNextPage, isLoading, isFetchingNextPage } = useInfiniteGetQuery({
endpoint: '/api/posts',
queryKey: ['posts'],
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
const posts = data?.pages.flatMap((page) => page.posts) ?? [];
return (
<FlatList
data={posts}
onEndReached={() => hasNextPage && fetchNextPage()}
// ... otros props
/>
);
}Mutations (POST, PUT, PATCH, DELETE)
import { usePostMutation, usePutMutation, useDeleteMutation } from '@alejo-dev/xavi-providers';
function UserForm() {
const createUser = usePostMutation({
endpoint: '/api/users',
onSuccess: (data) => {
console.log('Usuario creado:', data);
// Invalidar queries relacionadas
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
const updateUser = usePutMutation<UpdateInput>({
// El endpoint puede ser una función de las variables para URLs dinámicas
endpoint: (variables) => `/api/users/${variables.id}`,
onSuccess: () => {
// Actualizar cache optimísticamente
queryClient.setQueryData(['user', userId], updatedUser);
},
});
const deleteUser = useDeleteMutation<DeleteInput>({
endpoint: (variables) => `/api/users/${variables.id}`,
onSuccess: () => {
queryClient.removeQueries({ queryKey: ['user', userId] });
},
});
const handleSubmit = (userData) => {
createUser.mutate(userData);
};
return (
<Button
title="Crear Usuario"
onPress={() => handleSubmit(formData)}
disabled={createUser.isPending}
/>
);
}File Upload
import { useUploadMutation } from '@alejo-dev/xavi-providers';
function ImageUploader() {
const uploadImage = useUploadMutation({
endpoint: '/api/upload',
onSuccess: (data) => {
console.log('Imagen subida:', data.url);
},
});
const handleUpload = async (imageUri) => {
const formData = new FormData();
formData.append('image', {
uri: imageUri,
type: 'image/jpeg',
name: 'photo.jpg',
});
uploadImage.mutate(formData);
};
return <Button title="Subir Imagen" onPress={() => handleUpload(imageUri)} />;
}🌐 Uso de Hooks GraphQL
GraphQL Query
import { useGQLQuery } from '@alejo-dev/xavi-providers';
const GET_USER = `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading } = useGQLQuery({
document: GET_USER,
variables: { id: userId },
queryKey: ['user', userId],
});
return <Text>{data?.user?.name}</Text>;
}GraphQL Mutation
import { useGQLMutation } from '@alejo-dev/xavi-providers';
const CREATE_USER = `
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`;
function CreateUserForm() {
const createUser = useGQLMutation({
document: CREATE_USER,
onSuccess: (data) => {
console.log('Usuario creado:', data.createUser);
},
});
return <Button title="Crear Usuario" onPress={() => createUser.mutate({ input: formData })} />;
}📶 Gestión de Estado Offline
Estado del Servidor
import { useApiServerStatus } from '@alejo-dev/xavi-providers';
function ConnectionStatus() {
const {
status,
isServerReachable,
isCheckingConnection,
offlineReason,
consecutiveFailures,
lastOnlineAt,
lastOfflineAt,
} = useApiServerStatus();
return (
<View>
<Text>Estado: {status}</Text>
<Text>Conectado: {isServerReachable ? 'Sí' : 'No'}</Text>
<Text>Razón offline: {offlineReason}</Text>
<Text>Fallos consecutivos: {consecutiveFailures}</Text>
</View>
);
}Modo Offline Manual (Debugging)
import { useApiOfflineMode } from '@alejo-dev/xavi-providers';
function DebugControls() {
const {
isManualOfflineEnabled,
isEffectiveOfflineMode,
enableManualOfflineMode,
disableManualOfflineMode,
toggleManualOfflineMode,
} = useApiOfflineMode();
return (
<View>
<Text>Offline Manual: {isManualOfflineEnabled ? 'Activado' : 'Desactivado'}</Text>
<Text>Offline Efectivo: {isEffectiveOfflineMode ? 'Sí' : 'No'}</Text>
<Button
title={isManualOfflineEnabled ? 'Desactivar Offline' : 'Activar Offline'}
onPress={toggleManualOfflineMode}
/>
</View>
);
}Acciones del Servidor
import { useApiServerStatus } from '@alejo-dev/xavi-providers';
function ServerActions() {
const { checkServerConnection, markOnline, markOffline } = useApiServerStatus();
return (
<View>
<Button title="Verificar Conexión" onPress={checkServerConnection} />
<Button title="Marcar Online" onPress={() => markOnline()} />
<Button title="Marcar Offline" onPress={() => markOffline('manual-debug')} />
</View>
);
}🛠️ Utilidades
Crear Query Keys
import { createQueryKeys } from '@alejo-dev/xavi-providers';
export const userKeys = createQueryKeys('users');
// {
// all: () => ['users'],
// lists: () => ['users', 'list'],
// list: (filters) => ['users', 'list', 'serialized-filters'],
// details: () => ['users', 'detail'],
// detail: (id) => ['users', 'detail', id],
// infinite: () => ['users', 'infinite'],
// infiniteList: (filters) => ['users', 'infinite', 'serialized-filters'],
// }
// Los filtros se serializan automáticamente para mantener consistencia
const filters = { status: 'active', sort: 'name' };
userKeys.list(filters); // ['users', 'list', '{"sort":"name","status":"active"}']Detección de Errores de Conectividad
import { isConnectivityError, normalizeRestError } from '@alejo-dev/xavi-providers';
try {
// alguna petición
} catch (error) {
if (isConnectivityError(error)) {
console.log('Error de red - activando modo offline');
} else {
const normalizedError = normalizeRestError(error);
console.log('Error de aplicación:', normalizedError.message);
}
}Error Offline Manual
import { ManualOfflineError, isManualOfflineError } from '@alejo-dev/xavi-providers';
try {
// petición que falla en modo offline
} catch (error) {
if (isManualOfflineError(error)) {
console.log('Modo offline manual activado');
}
}📋 Estados del Servidor
| Estado | Descripción |
| ---------- | -------------------------------- |
| unknown | Estado inicial, sin verificación |
| online | Servidor reachable |
| checking | Verificando conectividad |
| offline | Servidor no reachable |
Razones de Offline
none- Conectadomanual-debug- Activado manualmentenetwork-error- Error de redping-failed- Ping fallóserver-unreachable- Servidor no responde
🔄 Ciclo de Vida de Peticiones
Todas las peticiones automáticamente:
- En éxito: Llaman
handleRequestSuccess()→ resetean contador de fallos - En fallo: Verifican si es error de conectividad → incrementan contador
- Después de N fallos: Disparan verificación de ping
- Ping falla: Marcan como offline
- Ping reintenta: Cada intervalo configurado
⚡ Mejores Prácticas
1. Query Keys Consistentes
// ✅ Bueno
const userKeys = createQueryKeys('users');
const { data } = useGetQuery({ queryKey: userKeys.detail(userId) });
// ❌ Evitar
const { data } = useGetQuery({ queryKey: ['user', userId] });2. Invalidación Inteligente
const createUser = usePostMutation({
endpoint: '/api/users',
onSuccess: () => {
// Invalidar lista de usuarios
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
},
});3. Cache Optimista
const updateUser = usePutMutation({
endpoint: `/api/users/${userId}`,
onMutate: async (newUser) => {
// Cancelar queries salientes
await queryClient.cancelQueries({ queryKey: userKeys.detail(userId) });
// Snapshot del valor anterior
const previousUser = queryClient.getQueryData(userKeys.detail(userId));
// Actualizar cache optimísticamente
queryClient.setQueryData(userKeys.detail(userId), newUser);
return { previousUser };
},
onError: (err, newUser, context) => {
// Revertir en caso de error
if (context?.previousUser) {
queryClient.setQueryData(userKeys.detail(userId), context.previousUser);
}
},
});4. Manejo de Errores
const { data, error, isError } = useGetQuery({
// ...
});
if (isError) {
if (isConnectivityError(error)) {
return <OfflineMessage />;
} else {
return <ErrorMessage error={normalizeRestError(error)} />;
}
}📊 Tipos TypeScript
// Configuración
type ApiClientConfig = {
pingEndpoint?: string;
pingMethod?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
pingHeaders?: Record<string, string>;
pingTimeoutMs?: number;
offlineRetryIntervalMs?: number;
enableServerHealthCheck?: boolean;
enableInitialPing?: boolean;
maxConsecutiveFailuresBeforeOffline?: number;
offlineStatusCodes?: number[];
pingResponseValidator?: (response: any) => boolean;
manualOfflineStrategy?: 'block-network' | 'serve-cache-and-block-network';
enableDebugOfflineControls?: boolean;
healthCheckMode?: 'always' | 'on-failure';
};
// Estado del servidor
type ServerStatus = 'unknown' | 'online' | 'checking' | 'offline';
type OfflineReason =
| 'none'
| 'manual-debug'
| 'network-error'
| 'ping-failed'
| 'server-unreachable';
// Errores
class ManualOfflineError extends Error {
constructor(message?: string);
}
function isManualOfflineError(error: any): error is ManualOfflineError;🐛 Debugging
Estado de React Query DevTools
Instala @tanstack/react-query-devtools para inspeccionar el estado del cache:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
export default function App() {
return (
<ApiClientProvider>
{/* Tu app */}
<ReactQueryDevtools initialIsOpen={false} />
</ApiClientProvider>
);
}📝 Notas de Migración
Si actualizas desde una versión anterior:
- Provider: El
ApiClientProviderahora acepta un propconfigopcional - Persistencia: La configuración de persistencia se mantiene igual
- Hooks: Todos los hooks existentes siguen funcionando
- Nuevos hooks: Los nuevos hooks están disponibles pero son opcionales
🤝 Contribuir
- Fork el repositorio
- Crea una branch para tu feature (
git checkout -b feature/amazing-feature) - Commit tus cambios (
git commit -m 'Add amazing feature') - Push a la branch (
git push origin feature/amazing-feature) - Abre un Pull Request
📄 Licencia
ISC
