Data fetching made easy. Check out the new category! 🚀


useKy

A powerful React hook for HTTP requests using Ky, featuring global provider, retry logic, timeout, abort control, and plugin system

useKy Hook Demo

GET Request
Returns data after a 3-second delay to demonstrate abort functionality.
Idle

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

PropTypeDefaultDescription
configKyConfig{}Global configuration for all Ky requests
childrenReactNodeChild components that will use the provider

KyConfig Interface

PropertyTypeDefaultDescription
prefixUrlstringundefinedPrefix URL for all relative requests
timeoutnumberundefinedRequest timeout in milliseconds
retriesnumber0Number of retry attempts on failure
headersHeadersInitundefinedDefault headers for all requests
methodstring'GET'Default HTTP method
searchParamsURLSearchParamsundefinedURL search parameters
jsonunknownundefinedRequest body as JSON
bodyBodyInitundefinedRequest body
modeRequestModeundefinedRequest mode (cors, no-cors, etc.)
credentialsRequestCredentialsundefinedRequest credentials
cacheRequestCacheundefinedRequest cache
redirectRequestRedirectundefinedRequest redirect
referrerstringundefinedRequest referrer
integritystringundefinedRequest integrity

useKy Hook

function useKy<T = any>(url: string, options?: KyOptions): KyResult<T>;

Parameters

ParameterTypeDefaultDescription
urlstringEndpoint URL to request data from
optionsKyOptions{}Ky options and hook configuration

KyOptions Interface

PropertyTypeDefaultDescription
jsonunknownRequest body serialized as JSON
headersHeadersInitRequest headers
timeoutnumberRequest timeout (overrides global)
retriesnumberRetry attempts (overrides global)
immediatebooleantrueExecute request immediately
methodstringHTTP method
searchParamsURLSearchParamsURL search parameters
pluginsKyPlugin[][]Array of plugins for request hooks
bodyBodyInitRequest body
modeRequestModeRequest mode
credentialsRequestCredentialsRequest credentials
cacheRequestCacheRequest cache
redirectRequestRedirectRequest redirect
referrerstringRequest referrer
integritystringRequest integrity

KyResult<T>

PropertyTypeDescription
dataT | nullFetched data or null on error/loading
errorHTTPError | TimeoutError | Error | nullError object if the request fails
loadingbooleanWhether the request is in progress
refetch() => Promise<T | null>Function to re-trigger the request
abort() => voidCancels the current in-flight request
abortedbooleanWhether 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>;
}
PropertyTypeWhen It ExecutesDescription
beforeRequest(url: string, options: Options) => void | Promise<void>Before each requestAllows modifying URL, headers, body, etc.
afterResponse(response: KyResponse) => void | Promise<void>After successful responseAllows processing response, cache, logs, etc.
onError(error: HTTPError | TimeoutError | Error) => void | Promise<void>When request error occursAllows error handling, custom retry, etc.

Key Features

  • 🔄 Async Support: All hooks support async/await operations
  • 📝 Options Modification: beforeRequest can directly modify the options 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

PropertyTypeDescription
configKyConfigCurrent global configuration
updateConfig(newConfig: Partial<KyConfig>) => voidFunction to update global config
instanceKyInstanceCurrent 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');