react-native-apikit
Version:
Modern API toolkit for React Native and Expo with automatic token management, smart response parsing, and built-in error handling
267 lines (266 loc) • 12.1 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.useApi = useApi;
exports.usePaginatedApi = usePaginatedApi;
const react_1 = require("react");
const config_1 = require("../config");
function useApi() {
const [state, setState] = (0, react_1.useState)({
loading: false,
error: null,
data: null,
});
const reset = (0, react_1.useCallback)(() => {
setState({
loading: false,
error: null,
data: null,
});
}, []);
const abortControllers = (0, react_1.useRef)({});
const makeRequest = (0, react_1.useCallback)((method, url, data, config) => __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e;
const apiConfig = (0, config_1.getApiKitConfig)();
const engine = (0, config_1.getEngine)();
if (!apiConfig.baseUrl) {
console.warn('ApiKit: baseUrl not configured. Please call configureApiKit first.');
return;
}
setState(prev => (Object.assign(Object.assign({}, prev), { loading: true, error: null })));
// Get token if storage is configured
let token = null;
if (apiConfig.tokenStorage) {
try {
token = yield apiConfig.tokenStorage.getToken();
}
catch (error) {
console.warn('ApiKit: Failed to get token from storage:', error);
}
}
// Prepare headers
const headers = Object.assign(Object.assign({ 'Content-Type': 'application/json' }, apiConfig.headers), config === null || config === void 0 ? void 0 : config.headers);
if (token) {
headers.Authorization = `Bearer ${token}`;
}
// Build full URL
const fullUrl = url.startsWith('http') ? url : `${apiConfig.baseUrl}${url}`;
// Add query parameters
let finalUrl = fullUrl;
if (config === null || config === void 0 ? void 0 : config.params) {
const params = new URLSearchParams();
Object.entries(config.params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
params.append(key, String(value));
}
});
const queryString = params.toString();
if (queryString) {
finalUrl += (fullUrl.includes('?') ? '&' : '?') + queryString;
}
}
// Setup AbortController
let controller;
let signal = config === null || config === void 0 ? void 0 : config.signal;
if (!signal) {
controller = new AbortController();
signal = controller.signal;
abortControllers.current[url] = controller;
}
// Prepare request config
const requestConfig = {
url: finalUrl,
method,
data: method !== 'GET' ? data : undefined,
headers,
timeout: (config === null || config === void 0 ? void 0 : config.timeout) || apiConfig.timeout,
signal,
};
const maxRetries = typeof apiConfig.retry === 'number' ? apiConfig.retry : 0;
let attempt = 0;
let lastError = null;
while (attempt <= maxRetries) {
try {
// --- Interceptor: onRequest ---
let interceptedRequestConfig = requestConfig;
if (apiConfig.onRequest) {
interceptedRequestConfig = yield apiConfig.onRequest(requestConfig);
}
// Caching logic (GET only)
const isGet = method === 'GET';
let cacheKey = '';
if (isGet && apiConfig.cache) {
cacheKey = url;
if (config === null || config === void 0 ? void 0 : config.params) {
cacheKey += '?' + Object.entries(config.params).map(([k, v]) => `${k}=${v}`).join('&');
}
const cached = yield apiConfig.cache.get(cacheKey);
if (cached) {
setState({ loading: false, error: null, data: cached.data });
return;
}
}
// Make request
const response = yield engine.request(interceptedRequestConfig);
// --- Interceptor: onResponse ---
let interceptedResponse = response;
if (apiConfig.onResponse) {
interceptedResponse = yield apiConfig.onResponse(response);
}
// Handle 401 globally
if (interceptedResponse.status === 401 && apiConfig.onUnauthorized) {
apiConfig.onUnauthorized();
}
// After successful GET, store in cache
if (isGet && apiConfig.cache && interceptedResponse.data !== undefined) {
const ttl = (config && config.cacheTtlMs) || 60000; // 1 min default
yield apiConfig.cache.set(cacheKey, { data: interceptedResponse.data, status: interceptedResponse.status }, ttl);
}
setState({
loading: false,
error: null,
data: interceptedResponse.data,
});
return;
}
catch (error) {
let apiError;
if (error.name === 'AbortError') {
apiError = {
message: 'Request cancelled',
isNetworkError: false,
};
}
else if ((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('Network request failed')) {
apiError = {
message: 'No internet connection',
isNetworkError: true,
};
}
else if (error.code === 'ECONNABORTED' || ((_b = error.message) === null || _b === void 0 ? void 0 : _b.includes('timeout'))) {
apiError = {
message: 'Request timeout',
isTimeout: true,
};
}
else {
apiError = {
message: error.message || 'An error occurred',
status: (_c = error.response) === null || _c === void 0 ? void 0 : _c.status,
details: (_d = error.response) === null || _d === void 0 ? void 0 : _d.data,
isUnauthorized: ((_e = error.response) === null || _e === void 0 ? void 0 : _e.status) === 401,
};
}
// --- Interceptor: onError ---
if (apiConfig.onError) {
apiError = yield apiConfig.onError(apiError);
}
lastError = apiError;
// Only retry on network error or timeout
if ((apiError.isNetworkError || apiError.isTimeout) && attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 200; // Exponential backoff: 200ms, 400ms, 800ms, ...
yield new Promise(res => setTimeout(res, delay));
attempt++;
continue;
}
setState({
loading: false,
error: apiError,
data: null,
});
return;
}
}
// If all retries failed
setState({
loading: false,
error: lastError,
data: null,
});
// After request completes, clean up controller
if (controller)
delete abortControllers.current[url];
}), []);
// Helper to wrap each method to return cancel
function withCancel(fn) {
return (...args) => {
let url = args[0];
let controller;
const promise = fn(...args);
if (abortControllers.current[url]) {
controller = abortControllers.current[url];
}
return {
promise,
cancel: () => controller === null || controller === void 0 ? void 0 : controller.abort(),
};
};
}
return Object.assign(Object.assign({}, state), { get: withCancel((url, config) => makeRequest('GET', url, undefined, config)), post: withCancel((url, data, config) => makeRequest('POST', url, data, config)), put: withCancel((url, data, config) => makeRequest('PUT', url, data, config)), patch: withCancel((url, data, config) => makeRequest('PATCH', url, data, config)), del: withCancel((url, config) => makeRequest('DELETE', url, undefined, config)), reset });
}
// Paginated API hook
function usePaginatedApi(url, { pageSize = 20, initialPage = 1, params = {}, config, } = {}) {
const [page, setPage] = (0, react_1.useState)(initialPage);
const [data, setData] = (0, react_1.useState)([]);
const [loading, setLoading] = (0, react_1.useState)(false);
const [error, setError] = (0, react_1.useState)(null);
const [hasMore, setHasMore] = (0, react_1.useState)(true);
const apiConfig = (0, config_1.getApiKitConfig)();
const engine = (0, config_1.getEngine)();
const fetchPage = (pageNum) => __awaiter(this, void 0, void 0, function* () {
setLoading(true);
setError(null);
try {
const fullParams = Object.assign(Object.assign({}, params), { page: pageNum, pageSize });
const fullUrl = url;
const headers = Object.assign(Object.assign({ 'Content-Type': 'application/json' }, apiConfig.headers), ((config === null || config === void 0 ? void 0 : config.headers) || {}));
const requestConfig = {
url: fullUrl,
method: 'GET',
params: fullParams,
headers,
timeout: (config === null || config === void 0 ? void 0 : config.timeout) || apiConfig.timeout,
signal: config === null || config === void 0 ? void 0 : config.signal,
};
const response = yield engine.request(requestConfig);
if (response.status === 401 && apiConfig.onUnauthorized) {
apiConfig.onUnauthorized();
}
setData(prev => (pageNum === 1 ? response.data : [...prev, ...response.data]));
setHasMore(response.data.length === pageSize);
}
catch (err) {
setError({ message: err.message || 'An error occurred' });
}
finally {
setLoading(false);
}
});
(0, react_1.useEffect)(() => {
fetchPage(page);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [page, url, JSON.stringify(params)]);
const nextPage = () => {
if (hasMore && !loading)
setPage(p => p + 1);
};
const prevPage = () => {
if (page > 1 && !loading)
setPage(p => p - 1);
};
const reset = () => {
setPage(initialPage);
setData([]);
setHasMore(true);
setError(null);
};
return { page, data, loading, error, hasMore, nextPage, prevPage, reset };
}