Installation
Usage
Wrap your app with KyProvider
import { KyProvider } from '@/hooks/use-ky';
function App() {
return (
<KyProvider
config={{
prefixUrl: 'https://api.example.com',
retries: 3,
timeout: 5000,
headers: {
'Content-Type': 'application/json',
},
}}
>
<YourApp />
</KyProvider>
);
}
Use the hook in your components
import { useKyGet } from '@/hooks/use-ky';
function UserProfile() {
const { data, loading, error } = useKyGet<User>('/users/me');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{data?.name}</div>;
}
API Reference
KyProvider
Props
Prop | Type | Default | Description |
---|---|---|---|
config | KyConfig | {} | Global configuration for all Ky requests |
children | ReactNode | — | Child components that will use the provider |
KyConfig
Interface
Property | Type | Default | Description |
---|---|---|---|
prefixUrl | string | undefined | Prefix URL for all relative requests |
timeout | number | undefined | Request timeout in milliseconds |
retries | number | 0 | Number of retry attempts on failure |
headers | HeadersInit | undefined | Default headers for all requests |
method | string | 'GET' | Default HTTP method |
searchParams | URLSearchParams | undefined | URL search parameters |
json | unknown | undefined | Request body as JSON |
body | BodyInit | undefined | Request body |
mode | RequestMode | undefined | Request mode (cors, no-cors, etc.) |
credentials | RequestCredentials | undefined | Request credentials |
cache | RequestCache | undefined | Request cache |
redirect | RequestRedirect | undefined | Request redirect |
referrer | string | undefined | Request referrer |
integrity | string | undefined | Request integrity |
useKy
Hook
function useKy<T = any>(url: string, options?: KyOptions): KyResult<T>;
Parameters
Parameter | Type | Default | Description |
---|---|---|---|
url | string | — | Endpoint URL to request data from |
options | KyOptions | {} | Ky options and hook configuration |
KyOptions
Interface
Property | Type | Default | Description |
---|---|---|---|
json | unknown | — | Request body serialized as JSON |
headers | HeadersInit | — | Request headers |
timeout | number | — | Request timeout (overrides global) |
retries | number | — | Retry attempts (overrides global) |
immediate | boolean | true | Execute request immediately |
method | string | — | HTTP method |
searchParams | URLSearchParams | — | URL search parameters |
plugins | KyPlugin[] | [] | Array of plugins for request hooks |
body | BodyInit | — | Request body |
mode | RequestMode | — | Request mode |
credentials | RequestCredentials | — | Request credentials |
cache | RequestCache | — | Request cache |
redirect | RequestRedirect | — | Request redirect |
referrer | string | — | Request referrer |
integrity | string | — | Request integrity |
KyResult<T>
Property | Type | Description |
---|---|---|
data | T | null | Fetched data or null on error/loading |
error | HTTPError | TimeoutError | Error | null | Error object if the request fails |
loading | boolean | Whether the request is in progress |
refetch | () => Promise<T | null> | Function to re-trigger the request |
abort | () => void | Cancels the current in-flight request |
aborted | boolean | Whether the request was aborted |
Plugin System
The useKy
plugin system allows you to extend and intercept HTTP request behavior in a flexible and reusable way. Plugins execute at specific points in the request lifecycle, providing complete control over the process.
How They Work
Plugins are executed sequentially in the order they are provided in the plugins
array. Each plugin can modify request behavior through three main hooks:
// Execution order:
1. beforeRequest (for each plugin, in order)
2. HTTP request is executed
3a. afterResponse (if success) - for each plugin, in order
3b. onError (if error) - for each plugin, in order
KyPlugin
Interface
interface KyPlugin {
beforeRequest?: (url: string, options: Options) => void | Promise<void>;
afterResponse?: (response: KyResponse) => void | Promise<void>;
onError?: (error: HTTPError | TimeoutError | Error) => void | Promise<void>;
}
Property | Type | When It Executes | Description |
---|---|---|---|
beforeRequest | (url: string, options: Options) => void | Promise<void> | Before each request | Allows modifying URL, headers, body, etc. |
afterResponse | (response: KyResponse) => void | Promise<void> | After successful response | Allows processing response, cache, logs, etc. |
onError | (error: HTTPError | TimeoutError | Error) => void | Promise<void> | When request error occurs | Allows error handling, custom retry, etc. |
Key Features
- 🔄 Async Support: All hooks support
async/await
operations - 📝 Options Modification:
beforeRequest
can directly modify theoptions
object - 🚫 Non-Blocking: Plugin errors don't prevent request execution
- 📊 Full Access: Plugins have access to URL, options, response, and errors
- 🔗 Composition: Multiple plugins can be easily combined
Common Use Cases
1. Automatic Authentication
const authPlugin: KyPlugin = {
beforeRequest: async (url, options) => {
// Get token from storage or refresh if needed
const token = await getValidToken();
if (token) {
options.headers = {
...options.headers,
Authorization: `Bearer ${token}`,
};
}
},
onError: async (error) => {
// If 401 error, try token refresh
if (error instanceof HTTPError && error.response.status === 401) {
await refreshToken();
}
},
};
2. Logging and Monitoring
const monitoringPlugin: KyPlugin = {
beforeRequest: (url, options) => {
console.log(`🚀 [${options.method || 'GET'}] ${url}`);
performance.mark(`request-start-${url}`);
},
afterResponse: (response) => {
const url = response.url;
performance.mark(`request-end-${url}`);
performance.measure(
`request-duration-${url}`,
`request-start-${url}`,
`request-end-${url}`,
);
console.log(
`✅ [${response.status}] ${url} - ${performance.getEntriesByName(`request-duration-${url}`)[0]?.duration}ms`,
);
},
onError: (error) => {
if (error instanceof HTTPError) {
console.error(`❌ [${error.response.status}] ${error.response.url}`);
} else {
console.error(`💥 Network error:`, error.message);
}
},
};
3. Smart Caching
const cachePlugin: KyPlugin = {
beforeRequest: async (url, options) => {
// Check cache only for GET requests
if (options.method === 'GET' || !options.method) {
const cached = await getCachedResponse(url);
if (cached && !isExpired(cached)) {
// Return cached response (specific implementation)
throw new CacheHitError(cached);
}
}
},
afterResponse: async (response) => {
// Cache only successful GET responses
if (response.status >= 200 && response.status < 300) {
await setCachedResponse(response.url, response.clone());
}
},
};
4. Rate Limiting
const rateLimitPlugin: KyPlugin = {
beforeRequest: async (url, options) => {
const rateLimiter = getRateLimiter(url);
// Wait if needed to respect rate limit
await rateLimiter.waitIfNeeded();
},
afterResponse: (response) => {
// Read rate limit headers from response
const remaining = response.headers.get('X-RateLimit-Remaining');
const resetTime = response.headers.get('X-RateLimit-Reset');
updateRateLimitInfo(response.url, { remaining, resetTime });
},
};
5. Data Transformation
const dataTransformPlugin: KyPlugin = {
beforeRequest: (url, options) => {
// Transform data before sending
if (options.json && typeof options.json === 'object') {
options.json = transformRequestData(options.json);
}
},
afterResponse: async (response) => {
// Intercept and transform response
if (response.headers.get('content-type')?.includes('application/json')) {
const data = await response.json();
const transformed = transformResponseData(data);
// Replace the response json() method
response.json = () => Promise.resolve(transformed);
}
},
};
Creating Reusable Plugins
To create reusable plugins, consider using factory functions:
// Plugin factory for different auth strategies
function createAuthPlugin(
strategy: 'bearer' | 'apikey',
options: AuthOptions,
): KyPlugin {
return {
beforeRequest: async (url, requestOptions) => {
const token = await getToken(strategy, options);
if (strategy === 'bearer') {
requestOptions.headers = {
...requestOptions.headers,
Authorization: `Bearer ${token}`,
};
} else if (strategy === 'apikey') {
requestOptions.headers = {
...requestOptions.headers,
'X-API-Key': token,
};
}
},
};
}
// Plugin factory for different analytics providers
function createAnalyticsPlugin(provider: 'mixpanel' | 'amplitude'): KyPlugin {
return {
afterResponse: (response) => {
const event = {
event: 'api_call_success',
properties: {
url: response.url,
status: response.status,
method: response.request?.method,
},
};
if (provider === 'mixpanel') {
mixpanel.track(event.event, event.properties);
} else if (provider === 'amplitude') {
amplitude.track(event);
}
},
};
}
// Usage
const plugins = [
createAuthPlugin('bearer', { tokenKey: 'access_token' }),
createAnalyticsPlugin('mixpanel'),
];
Plugin Composition
// Combine multiple plugins for complete functionality
const productionPlugins = [
authPlugin, // Automatic authentication
monitoringPlugin, // Logs and metrics
cachePlugin, // Smart caching
rateLimitPlugin, // Rate limiting
analyticsPlugin, // Analytics
];
function ApiComponent() {
const { data, loading, error } = useKyGet<ApiData>('/api/data', {
plugins: productionPlugins,
});
// ... rest of component
}
Important Considerations
⚡ Performance
- Plugins run for every request - keep them lightweight
- Use
async/await
only when necessary - Avoid expensive operations in
beforeRequest
🔒 Security
const secureAuthPlugin: KyPlugin = {
beforeRequest: (url, options) => {
// ✅ Good: check for HTTPS before adding sensitive tokens
if (url.startsWith('https://')) {
options.headers = {
...options.headers,
Authorization: `Bearer ${getToken()}`,
};
}
// ❌ Avoid: exposing tokens on insecure URLs
},
};
📝 Mutability
const plugin: KyPlugin = {
beforeRequest: (url, options) => {
// ✅ Good: modify options directly
options.headers = { ...options.headers, 'X-Custom': 'value' };
// ❌ Avoid: trying to modify URL (read-only)
// url = 'https://different-url.com'; // Won't work
},
};
🚨 Error Handling
const robustPlugin: KyPlugin = {
beforeRequest: async (url, options) => {
try {
const token = await getToken();
options.headers = {
...options.headers,
Authorization: `Bearer ${token}`,
};
} catch (error) {
// ✅ Good: don't fail the request due to plugin error
console.warn('Failed to get auth token:', error);
}
},
};
🔄 Plugin Dependencies
// ✅ Plugins can depend on execution order
const plugins = [
authPlugin, // 1. Add auth headers
loggingPlugin, // 2. Log with complete headers
cachePlugin, // 3. Check cache after auth
];
Convenience Hooks
useKyGet
function useKyGet<T = any>(url: string, options?: KyOptions): KyResult<T>;
useKyPost
function useKyPost<T = any>(
url: string,
data?: any,
options?: KyOptions,
): KyResult<T>;
useKyPut
function useKyPut<T = any>(
url: string,
data?: any,
options?: KyOptions,
): KyResult<T>;
useKyDelete
function useKyDelete<T = any>(url: string, options?: KyOptions): KyResult<T>;
useKyPatch
function useKyPatch<T = any>(
url: string,
data?: any,
options?: KyOptions,
): KyResult<T>;
useKyContext
Hook
function useKyContext(): KyContextValue;
Returns the current Ky context value with configuration and utilities.
KyContextValue
Property | Type | Description |
---|---|---|
config | KyConfig | Current global configuration |
updateConfig | (newConfig: Partial<KyConfig>) => void | Function to update global config |
instance | KyInstance | Current Ky instance |
request | (url: string, options?: Options) => Promise<KyResponse> | Enhanced request function |
useKyInstance
Hook
function useKyInstance(): {
instance: KyInstance;
request: (url: string, options?: Options) => Promise<KyResponse>;
get: <T>(url: string, options?: Options) => Promise<T>;
post: <T>(url: string, data?: any, options?: Options) => Promise<T>;
put: <T>(url: string, data?: any, options?: Options) => Promise<T>;
delete: <T>(url: string, options?: Options) => Promise<T>;
patch: <T>(url: string, data?: any, options?: Options) => Promise<T>;
};
Returns utilities for making requests without state management.
Key Features
🌐 Global Provider
- Centralized configuration for all HTTP requests.
- Shared settings like prefix URL, headers, timeout, and retries.
- Dynamic configuration updates at runtime.
🔌 Plugin System
- Extensible plugin architecture for request/response interceptors.
- Support for logging, authentication, caching, and custom logic.
- Async plugin support for complex operations.
🔄 Automatic Retry
- Configurable retry attempts with intelligent logic.
- Skips retries on client errors and cancellations.
- Exponential backoff support via Ky.
⏱️ Timeout Support
- Request-level and global timeout settings.
- Automatic request cancellation on timeout.
🛑 Abort Control
- Manual request cancellation via
abort()
. - Automatic cleanup on component unmount.
- AbortController integration for proper cancellation.
🎯 Type Safety
- Full TypeScript support with generics.
- Typed request/response handling.
- Comprehensive error type definitions.
🚀 Performance
- Efficient memoization to prevent unnecessary re-renders.
- Optimized for concurrent React features.
- Automatic request deduplication.
📊 State Management
- Built-in loading, error, and data states.
- Automatic state cleanup on unmount.
- Proper handling of race conditions.
Examples
Basic GET Request
const { data, loading, error } = useKyGet<Post[]>('/posts');
if (loading) return <div>Loading posts...</div>;
if (error) return <div>Failed to load posts: {error.message}</div>;
return (
<div>
{data?.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
POST Request with Data
function CreatePost() {
const { data, loading, refetch } = useKyPost<Post>(
'/posts',
{ title: 'New Post', body: 'Content', userId: 1 },
{ immediate: false },
);
return (
<button onClick={() => refetch()} disabled={loading}>
{loading ? 'Creating...' : 'Create Post'}
</button>
);
}
Manual Requests with useKyInstance
function DataManager() {
const { get, post, put, delete: del } = useKyInstance();
const [users, setUsers] = useState<User[]>([]);
const fetchUsers = async () => {
try {
const userData = await get<User[]>('/users');
setUsers(userData);
} catch (error) {
console.error('Failed to fetch users:', error);
}
};
const createUser = async (userData: Partial<User>) => {
try {
const newUser = await post<User>('/users', userData);
setUsers((prev) => [...prev, newUser]);
} catch (error) {
console.error('Failed to create user:', error);
}
};
return (
<div>
<button onClick={fetchUsers}>Fetch Users</button>
<button onClick={() => createUser({ name: 'John Doe' })}>
Create User
</button>
</div>
);
}
Using Plugins
// Basic logging plugin
const loggingPlugin: KyPlugin = {
beforeRequest: (url, options) => {
console.log(`📤 ${options.method || 'GET'} ${url}`);
},
afterResponse: (response) => {
console.log(`📥 ${response.status} ${response.url}`);
},
onError: (error) => {
console.error('❌ Request failed:', error.message);
},
};
// Basic authentication plugin
const authPlugin: KyPlugin = {
beforeRequest: (url, options) => {
const token = localStorage.getItem('authToken');
if (token) {
options.headers = {
...options.headers,
Authorization: `Bearer ${token}`,
};
}
},
};
function UserProfile() {
const { data, loading, error } = useKyGet<User>('/user/profile', {
plugins: [authPlugin, loggingPlugin], // authPlugin first!
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>Welcome, {data?.name}!</div>;
}
💡 Tip: For more advanced plugin examples (cache, rate limiting, monitoring), see the Plugin System section above.
With Retry and Timeout
const { data, loading, error } = useKyGet<Data>('/api/unreliable-endpoint', {
retries: 5,
timeout: 10000,
});
Abort Request
function CancellableRequest() {
const { data, loading, abort, aborted } =
useKyGet<Data>('/api/slow-endpoint');
return (
<div>
{loading && !aborted && <div>Loading...</div>}
{aborted && <div>Request was cancelled</div>}
{data && <div>Data: {JSON.stringify(data)}</div>}
<button onClick={abort} disabled={!loading}>
Cancel Request
</button>
</div>
);
}
Dynamic Configuration Updates
function AuthManager() {
const { updateConfig } = useKyContext();
const login = async (token: string) => {
// Update global configuration with auth token
updateConfig({
headers: {
Authorization: `Bearer ${token}`,
},
});
};
const logout = () => {
// Remove auth token from global config
updateConfig({
headers: {},
});
};
return (
<div>
<button onClick={() => login('your-auth-token')}>Login</button>
<button onClick={logout}>Logout</button>
</div>
);
}
Error Handling
import { HTTPError, TimeoutError } from 'ky';
function DataWithErrorHandling() {
const { data, error, refetch } = useKyGet<ApiData>('/api/data');
const handleError = () => {
if (error instanceof HTTPError) {
switch (error.response.status) {
case 401:
return 'Unauthorized - please login';
case 404:
return 'Data not found';
case 500:
return 'Server error - please try again later';
default:
return `HTTP Error: ${error.response.status}`;
}
}
if (error instanceof TimeoutError) {
return 'Request timed out - please check your connection';
}
return 'An unexpected error occurred';
};
if (error) {
return (
<div>
<p>{handleError()}</p>
<button onClick={() => refetch()}>Retry</button>
</div>
);
}
return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}
Conditional Requests
function ConditionalData({ userId }: { userId?: string }) {
const { data, loading } = useKyGet<User>(userId ? `/users/${userId}` : '', {
immediate: !!userId, // Only fetch if userId is provided
});
if (!userId) return <div>Please select a user</div>;
if (loading) return <div>Loading user...</div>;
return <div>User: {data?.name}</div>;
}
Error Types
The hook handles three main types of errors:
HTTPError
Thrown for HTTP error status codes (4xx, 5xx).
if (error instanceof HTTPError) {
console.log(error.response.status); // 404, 500, etc.
console.log(error.response.statusText); // Not Found, Internal Server Error, etc.
}
TimeoutError
Thrown when a request exceeds the specified timeout.
if (error instanceof TimeoutError) {
console.log('Request timed out');
}
Error
Generic error for network issues, abort signals, etc.
if (error instanceof Error && error.name === 'AbortError') {
console.log('Request was aborted');
}
Best Practices
1. Use Plugins for Cross-Cutting Concerns
// Create reusable plugins for common functionality
const analyticsPlugin: KyPlugin = {
afterResponse: (response) => {
analytics.track('api_call_success', {
url: response.url,
status: response.status,
});
},
onError: (error) => {
analytics.track('api_call_error', {
error: error.message,
});
},
};
2. Centralize Configuration
// Create a custom provider with your app's defaults
function AppKyProvider({ children }: { children: ReactNode }) {
return (
<KyProvider
config={{
prefixUrl: process.env.NEXT_PUBLIC_API_URL,
timeout: 30000,
retries: 2,
headers: {
'Content-Type': 'application/json',
},
}}
>
{children}
</KyProvider>
);
}
3. Handle Loading States Properly
function DataComponent() {
const { data, loading, error, refetch } = useKyGet<Data>('/data');
// Show skeleton during initial load
if (loading && !data) return <Skeleton />;
// Show error state with retry option
if (error) return <ErrorState onRetry={refetch} />;
// Show data with loading indicator for refetch
return (
<div>
{loading && <LoadingIndicator />}
<DataDisplay data={data} />
</div>
);
}
4. Use TypeScript Interfaces
interface User {
id: number;
name: string;
email: string;
}
interface ApiResponse<T> {
data: T;
message: string;
success: boolean;
}
const { data } = useKyGet<ApiResponse<User[]>>('/users');