Comunicación con APIs
Cliente HTTP Personalizado
Hook useApi
// src/hooks/useApi.tsx
export const useApi = () => {
const { user } = useSelector(getUser);
const { showNotification } = useNotifications();
const baseRequest = async (
method: string,
endpoint: string,
data?: any,
requiresAuth = true
) => {
try {
const headers: any = {
'Content-Type': 'application/json',
};
if (requiresAuth && user?.token) {
headers['Authorization'] = `Bearer ${user.token}`;
}
const config = {
method,
url: `${BASE_URL}/${endpoint}`,
headers,
...(data && { data })
};
const response = await axios(config);
return response.data;
} catch (error) {
handleApiError(error);
throw error;
}
};
return {
get_req: (endpoint: string, requiresAuth = true) =>
baseRequest('GET', endpoint, null, requiresAuth),
post_req: (endpoint: string, data: any, requiresAuth = true) =>
baseRequest('POST', endpoint, data, requiresAuth),
put_req: (endpoint: string, data: any, requiresAuth = true) =>
baseRequest('PUT', endpoint, data, requiresAuth),
delete_req: (endpoint: string, requiresAuth = true) =>
baseRequest('DELETE', endpoint, null, requiresAuth)
};
};
Configuración Base
// src/services/request.ts
import axios from 'axios';
export const BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
// Interceptor para requests
axios.interceptors.request.use(
(config) => {
// Agregar timestamp a todas las requests
config.params = {
...config.params,
_t: Date.now()
};
return config;
},
(error) => Promise.reject(error)
);
// Interceptor para responses
axios.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Token expirado - logout automático
store.dispatch(logout());
window.location.href = '/auth/sign-in';
}
return Promise.reject(error);
}
);
Error Handling
Manejo Centralizado de Errores
// src/utils/errorHandling.ts
export const handleApiError = (error: any) => {
if (error.response?.status === 401) {
// Token expirado - logout automático
store.dispatch(logout());
router.push('/auth/sign-in');
} else if (error.response?.status === 403) {
// Sin permisos
showNotification('error', 'No tienes permisos para esta acción');
} else if (error.response?.status === 422) {
// Errores de validación
const errors = error.response.data.errors;
Object.keys(errors).forEach(field => {
showNotification('error', `${field}: ${errors[field][0]}`);
});
} else {
// Error general
const message = error.response?.data?.message || 'Error inesperado';
showNotification('error', message);
}
};
Error Boundaries
// components/ErrorBoundary.tsx
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends React.Component<
React.PropsWithChildren<{}>,
ErrorBoundaryState
> {
constructor(props: React.PropsWithChildren<{}>) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<Alert status="error">
<AlertIcon />
<AlertTitle>Something went wrong!</AlertTitle>
<AlertDescription>
{this.state.error?.message || 'An unexpected error occurred'}
</AlertDescription>
</Alert>
);
}
return this.props.children;
}
}
Servicios Específicos
Servicio de Propiedades
// src/services/property.ts
export const propertyService = {
getAll: async () => {
const { get_req } = useApi();
return await get_req('property/all');
},
getById: async (id: string) => {
const { get_req } = useApi();
return await get_req(`property/get?property_id=${id}`);
},
create: async (propertyData: CreatePropertyData) => {
const { post_req } = useApi();
return await post_req('property/create', propertyData);
},
update: async (id: string, propertyData: UpdatePropertyData) => {
const { put_req } = useApi();
return await put_req('property/edit', { property_id: id, ...propertyData });
},
delete: async (id: string) => {
const { delete_req } = useApi();
return await delete_req(`property/delete?property_id=${id}`);
}
};
Servicio de Autenticación
// src/services/auth.ts
export const authService = {
login: async (credentials: LoginCredentials) => {
const { post_req } = useApi();
return await post_req('auth/login', credentials, false);
},
logout: async () => {
const { post_req } = useApi();
return await post_req('auth/logout', {});
},
refreshToken: async () => {
const { post_req } = useApi();
return await post_req('auth/refresh', {});
},
resetPassword: async (email: string) => {
const { post_req } = useApi();
return await post_req('auth/reset-password', { email }, false);
}
};
React Query Integration
Configuración de React Query
// app/providers.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutos
retry: 3,
refetchOnWindowFocus: false
}
}
});
export function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<Provider store={store}>
{children}
</Provider>
</QueryClientProvider>
);
}
Custom Hooks con React Query
// hooks/useProperties.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
export const useProperties = () => {
return useQuery({
queryKey: ['properties'],
queryFn: propertyService.getAll,
staleTime: 5 * 60 * 1000
});
};
export const useProperty = (id: string) => {
return useQuery({
queryKey: ['property', id],
queryFn: () => propertyService.getById(id),
enabled: !!id
});
};
export const useCreateProperty = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: propertyService.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['properties'] });
showNotification('success', 'Propiedad creada exitosamente');
},
onError: (error) => {
handleApiError(error);
}
});
};
Caching Strategy
Cache Keys
// utils/queryKeys.ts
export const queryKeys = {
properties: ['properties'] as const,
property: (id: string) => ['property', id] as const,
users: ['users'] as const,
user: (id: string) => ['user', id] as const,
contracts: ['contracts'] as const,
contract: (id: string) => ['contract', id] as const
};
Cache Invalidation
// hooks/useCacheInvalidation.ts
export const useCacheInvalidation = () => {
const queryClient = useQueryClient();
const invalidateProperties = () => {
queryClient.invalidateQueries({ queryKey: queryKeys.properties });
};
const invalidateProperty = (id: string) => {
queryClient.invalidateQueries({ queryKey: queryKeys.property(id) });
};
const removeProperty = (id: string) => {
queryClient.removeQueries({ queryKey: queryKeys.property(id) });
};
return {
invalidateProperties,
invalidateProperty,
removeProperty
};
};
Loading States
Global Loading
// hooks/useGlobalLoading.ts
export const useGlobalLoading = () => {
const [isLoading, setIsLoading] = useState(false);
const withLoading = async <T>(asyncOperation: () => Promise<T>): Promise<T> => {
setIsLoading(true);
try {
const result = await asyncOperation();
return result;
} finally {
setIsLoading(false);
}
};
return { isLoading, withLoading };
};
Component Loading States
// components/PropertyList.tsx
export const PropertyList = () => {
const { data: properties, isLoading, error } = useProperties();
if (isLoading) {
return <Spinner size="xl" />;
}
if (error) {
return (
<Alert status="error">
<AlertIcon />
Error al cargar propiedades
</Alert>
);
}
return (
<Grid templateColumns="repeat(auto-fill, minmax(300px, 1fr))" gap={6}>
{properties?.map(property => (
<PropertyCard key={property.id} property={property} />
))}
</Grid>
);
};
File Upload
Upload Helper
// utils/fileUpload.ts
export const uploadFile = async (file: File, endpoint: string): Promise<string> => {
const formData = new FormData();
formData.append('file', file);
const { post_req } = useApi();
const response = await post_req(endpoint, formData, true);
return response.data.url;
};
Upload Component
// components/FileUpload.tsx
export const FileUpload = ({ onUpload }: { onUpload: (url: string) => void }) => {
const [isUploading, setIsUploading] = useState(false);
const handleFileSelect = async (files: FileList | null) => {
if (!files || files.length === 0) return;
setIsUploading(true);
try {
const file = files[0];
const url = await uploadFile(file, 'upload/property-image');
onUpload(url);
showNotification('success', 'Archivo subido exitosamente');
} catch (error) {
handleApiError(error);
} finally {
setIsUploading(false);
}
};
return (
<Input
type="file"
accept="image/*"
onChange={(e) => handleFileSelect(e.target.files)}
disabled={isUploading}
/>
);
};