A TypeScript-first GraphQL client for React applications with built-in caching, subscriptions, and testing utilities.
- 🚀 Multiple GraphQL endpoints - Support for multiple clients/endpoints
- 💾 Smart caching - Configurable caching strategies for queries
- 🔄 Real-time subscriptions - WebSocket support with graphql-ws protocol
- 🧪 Testing utilities - Built-in test client for easy mocking
- 📝 TypeScript first - Full TypeScript support with type safety
- ⚛️ React hooks -
useQueryanduseMutationhooks for seamless React integration - 🔧 Flexible configuration - Customizable request transformation and error handling
- Features
- Installation
- Quick Start
- Advanced Configuration
- Usage
- Subscription Timeout Strategies
- Subscription Reconnection Strategies
- API Reference
- Testing
- GraphQL Codegen Support
- Creating Request Objects
- Troubleshooting
- Credits
# npm
npm install @shane32/graphql
# yarn
yarn add @shane32/graphql
# pnpm
pnpm add @shane32/graphqlThis package requires the following peer dependencies:
react>= 16react-dom>= 16graphql>= 16 (optional, only needed for testing viaGraphQLTestClient)
This package uses the Fetch API and will require a polyfill for older browsers. When running in a test environment, you will need to provide a mock implementation of the Fetch API.
import React from 'react';
import { GraphQLClient, GraphQLContext, useQuery, IdleTimeoutStrategy } from '@shane32/graphql';
// 1. Create a client
const client = new GraphQLClient({
url: 'https://api.example.com/graphql'
});
// 2. Provide context to your app
function App() {
return (
<GraphQLContext.Provider value={{ client }}>
<UserProfile userId="123" />
</GraphQLContext.Provider>
);
}
// 3. Use hooks in components
function UserProfile({ userId }: { userId: string }) {
const { data, error, loading } = useQuery<{ user: { name: string; email: string } }>(
`query GetUser($id: ID!) {
user(id: $id) { name email }
}`,
{ variables: { id: userId } }
);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{data.user.name}</h1>
<p>{data.user.email}</p>
</div>
);
}- Set up a client in your index.tsx page
const client = new GraphQLClient({
url: 'https://localhost/graphql', // required; url of GraphQL endpoint
webSocketUrl: 'wss://localhost/graphql', // optional; url of GraphQL WebSocket endpoint
isForm: true, // optional; whether to use form data for POST requests instead of JSON
defaultFetchPolicy: 'cache-first', // optional; default is cache-first; other options are no-cache and cache-and-network
defaultCacheTime: 24 * 60 * 60 * 1000, // optional; specified in milliseconds; default is 1 day
maxCacheSize: 20 * 1024 * 1024, // optional; specified in bytes; default is 20MB
validateResponseContentType: true, // optional; validate response content type; default is false
// optional; transformation of Request; used to provide authentication information to request
transformRequest: request => Promise.resolve(request),
// optional; provides payload for WebSocket connection initialization messages; used to provide authentication information to request
generatePayload: () => Promise.resolve({}),
// optional; default options for subscriptions
defaultSubscriptionOptions: {
timeoutStrategy: new IdleTimeoutStrategy(30000) // abort subscription if no messages received for 30 seconds
},
// optional; callback for logging non-2xx HTTP status codes
logHttpError: (request, response) => {
console.error(`GraphQL request failed with status ${response.status} ${response.statusText}`);
response.text().then(body => console.error(`Response body: ${body}`));
},
// optional; callback for logging WebSocket connection failures
logWebSocketConnectionError: (request, connectionMessage, receivedMessage) => {
console.error(`WebSocket connection failed`, {
request,
connectionMessage,
receivedMessage
});
}
});- Set up context for hooks
const context: IGraphQLContext = {
client: client, // required: default client
guest: guestClient, // optional: guest client
"alt": altClient // optional: any other clients
};- Provide context to application
<GraphQLContext.Provider value={context}>
...
</GraphQLContext.Provider>It is simplest to set up your query and types first.
const productQuery = 'query($id: ID!) { product(id: $id) { name price } }';
interface ProductQueryResult {
product: { name: string, price: number } | null;
}
interface ProductQueryVariables {
id: string
}
const updateProductPriceMutation = 'mutation($id: ID!, $price: Float!) { updateProductPrice(id: $id, price: $price) { name price } }';
interface ProductPriceMutationResult {
updateProductPrice: { name: string, price: number } | null;
}
interface ProductPriceMutationVariables {
id: string;
price: number;
}
const priceUpdateSubscription = 'subscription($id: ID!) { priceUpdate(id: $id) { price } }';
interface ProductPriceSubscriptionResult {
priceUpdate: { price: number };
}
interface ProductPriceSubscriptionVariables {
id: string;
}Then you can use the client directly, or use one of the hooks.
// pull from context, if applicable
const client = React.useContext(GraphQLContext).client;
// execute a query or mutation
const result = await client.executeQueryRaw<ProductQueryResult, ProductQueryVariables>({ query: productQuery, variables: { id: productId } }).result;
// execute a subscription
const { connected, abort } = client.executeSubscription<ProductPriceSubscriptionResult, ProductPriceSubscriptionVariables>(
{ query: priceUpdateSubscription, variables: { id: productId } },
(data) => { /* process data or errors sent from the server */ },
(reason) => { /* subscription closed */ }
);const ProductComponent = ({ productId }) => {
const { error, data, refetch } = useQuery<ProductQueryResult, ProductQueryVariables>(productQuery, { variables: { id: productId } });
// display message if failed to retrieve data, with button to retry
if (error) return <ErrorDisplay onClick={refetch}>{error.message}</ErrorDisplay>;
// display loading if waiting for data to load
if (!data) return <Loading />;
return (
<div>
<h3>{data.product.name}</h3>
<p>Price: ${data.product.price}</p>
</div>
);
};const UpdateProductPriceComponent = ({ productId }) => {
const [updateProductPrice] = useMutation<ProductPriceMutationResult, ProductPriceMutationVariables>(updateProductPriceMutation);
const handleSubmit = async (newPrice) => {
try {
const ret = await updateProductPrice({ variables: { id: productId, price: newPrice } });
alert('Saved!');
} catch {
alert('Failure');
}
};
return (
<div>
<button onClick={() => handleSubmit(50)}>Update Price to $50</button>
</div>
);
};The useSubscription hook provides a React-friendly way to execute GraphQL subscriptions with manual control over when the subscription starts and stops.
const ProductPriceUpdateComponent = ({ productId }) => {
const [subscribe] = useSubscription<ProductPriceSubscriptionResult, ProductPriceSubscriptionVariables>(
priceUpdateSubscription,
{
variables: { id: productId },
onData: (data) => {
if (data.data) {
console.log("New price:", data.data.priceUpdate.price);
} else {
console.error("Error:", data.errors);
}
},
onClose: (reason) => {
console.log("Subscription closed:", reason);
},
onOpen: () => {
console.log("Subscription connected");
}
}
);
useEffect(() => {
const { abort } = subscribe();
return () => {
abort();
};
}, [subscribe]); // subscribe is stable so long as the productId does not change
return (
<div>
<p>Listening for price updates...</p>
</div>
);
};The useAutoSubscription hook provides automatic subscription lifecycle management with built-in reconnection capabilities. The subscription automatically connects when the component mounts and disconnects when it unmounts.
import { useAutoSubscription, AutoSubscriptionState } from '@shane32/graphql';
const ProductPriceUpdateComponent = ({ productId }) => {
const { state } = useAutoSubscription<ProductPriceSubscriptionResult, ProductPriceSubscriptionVariables>(
priceUpdateSubscription,
{
variables: { id: productId },
onData: (data) => {
if (data.data) {
console.log("New price:", data.data.priceUpdate.price);
} else {
console.error("Error:", data.errors);
}
},
onOpen: () => {
console.log("Subscription connected");
},
onClose: (reason) => {
console.log("Subscription closed:", reason);
},
enabled: true, // Can be used to enable/disable the subscription
reconnectionStrategy: new BackoffReconnectionStrategy(1000, 30000, 2.0, 10, true)
}
);
return (
<div>
<p>Status: {state}</p>
</div>
);
};The client supports configurable timeout strategies for subscriptions to handle connection reliability and heartbeat management. You can set a default timeout strategy for all subscriptions or specify one per subscription.
IdleTimeoutStrategy aborts the subscription if no inbound messages are received within the specified timeout period.
import { IdleTimeoutStrategy } from '@shane32/graphql';
// Abort subscription if no messages received for 30 seconds
const idleStrategy = new IdleTimeoutStrategy(30000);CorrelatedPingStrategy sends periodic pings and expects matching pongs within a deadline. Aborts if pong is not received in time.
import { CorrelatedPingStrategy } from '@shane32/graphql';
// Parameters: ackTimeoutMs, pingIntervalMs, pongDeadlineMs
const pingStrategy = new CorrelatedPingStrategy(5000, 10000, 3000);You can set the default timeout strategy within the defaultSubscriptionOptions configuration setting as follows:
const client = new GraphQLClient({
url: 'https://api.example.com/graphql',
webSocketUrl: 'wss://api.example.com/graphql',
defaultSubscriptionOptions: {
timeoutStrategy: new IdleTimeoutStrategy(30000)
}
});Alternatively, you can set the timeout strategy for a specific subscription when using useSubscription, useAutoSubscription or executeSubscription:
const [subscribe] = useSubscription(subscription, {
variables: { id: "123" },
timeoutStrategy: new CorrelatedPingStrategy(5000, 10000, 3000),
onData: (data) => console.log(data),
onClose: () => console.log("Subscription closed")
});The useAutoSubscription hook supports configurable reconnection strategies to handle connection failures and automatic reconnection. You can set a default reconnection strategy for all auto-subscriptions or specify one per subscription.
Fixed Delay Reconnection - A simple strategy that waits a fixed delay between reconnection attempts:
// Reconnect after 5 seconds
const { state } = useAutoSubscription(subscription, {
variables: { id: "123" },
reconnectionStrategy: 5000, // Simple number for fixed delay
onData: (data) => console.log(data)
});DelayedReconnectionStrategy - A more configurable fixed delay strategy:
import { DelayedReconnectionStrategy } from '@shane32/graphql';
// Wait 3 seconds between attempts, maximum 8 attempts
const delayedStrategy = new DelayedReconnectionStrategy(3000, 8);
const { state } = useAutoSubscription(subscription, {
variables: { id: "123" },
reconnectionStrategy: delayedStrategy,
onData: (data) => console.log(data)
});BackoffReconnectionStrategy - Implements exponential backoff with optional jitter to prevent thundering herd problems:
import { BackoffReconnectionStrategy } from '@shane32/graphql';
// Parameters: initialDelayMs, maxDelayMs, backoffMultiplier, maxAttempts, jitterEnabled
const backoffStrategy = new BackoffReconnectionStrategy(1000, 30000, 2.0, 10, true);
const { state } = useAutoSubscription(subscription, {
variables: { id: "123" },
reconnectionStrategy: backoffStrategy,
onData: (data) => console.log(data)
});The BackoffReconnectionStrategy also provides preset configurations:
// Aggressive reconnection for quick recovery
const aggressive = BackoffReconnectionStrategy.createAggressive();
// Conservative reconnection to reduce server load
const conservative = BackoffReconnectionStrategy.createConservative();You can set the default reconnection strategy within the defaultSubscriptionOptions configuration setting:
const client = new GraphQLClient({
url: 'https://api.example.com/graphql',
webSocketUrl: 'wss://api.example.com/graphql',
defaultSubscriptionOptions: {
reconnectionStrategy: new BackoffReconnectionStrategy(1000, 30000, 2.0, 10, true)
}
});Reconnection Behavior:
- Reconnection strategies only apply to
useAutoSubscription - Server-initiated closures (normal completion or errors) do not trigger reconnection
- Client-initiated closures (manual abort) do not trigger reconnection
- Network failures and unexpected disconnections will trigger reconnection based on the strategy
- Each connection maintains independent reconnection state
If you want to add GraphQL Codegen to your project, refer to CODEGEN-README.md
This package exports two GraphQL tag functions: gql for code generation only (throws at runtime), and gqlcompat for runtime use. Use gql in separate .queries.ts files with GraphQL Code Generator to generate typed documents, then import the generated documents. Use gqlcompat when you need to construct queries at runtime without code generation.
// For codegen - will throw at runtime
import { gql } from '@shane32/graphql';
gql`query GetUser { ... }`; // Save within .queries.ts file
// For runtime use
import { gqlcompat as gql } from '@shane32/graphql';
const query = gql`query GetUser { ... }`;You can use the createRequest function to construct GraphQL request objects that conform to the IGraphQLRequest interface:
import { createRequest } from '@shane32/graphql';
// With a string query
const request = createRequest(
`query GetProduct($id: ID!) { product(id: $id) { name price } }`,
{
variables: { id: "123" },
extensions: { persistedQuery: true },
operationName: "GetProduct"
}
);
// With a TypedDocumentString (from codegen)
const request = createRequest(
GetProductDocument,
{
variables: { id: "123" },
extensions: { persistedQuery: true }
}
);
// Use with a client
const result = await client.executeQueryRaw(request).result;This is useful when you need to create request objects outside of the provided hooks, or when building custom GraphQL client implementations.
| Parameter | Default | Description |
|---|---|---|
url (required) |
- | GraphQL endpoint URL |
webSocketUrl |
- | WebSocket endpoint URL for subscriptions |
defaultFetchPolicy |
'cache-first' |
Default caching strategy. Options: 'cache-first', 'no-cache', 'cache-and-network' |
defaultCacheTime |
86400000 |
Cache duration in milliseconds (24 hours) |
maxCacheSize |
20971520 |
Maximum cache size in bytes (20MB) |
asForm |
false |
Use form data instead of JSON for requests |
sendDocumentIdAsQuery |
false |
Include documentId as query parameter instead of POST body |
validateResponseContentType |
false |
Validate response content type before parsing* |
transformRequest |
- | Transform requests (e.g., add auth headers) |
generatePayload |
- | Generate WebSocket connection payload |
defaultSubscriptionOptions |
- | Default options for subscriptions (timeout & reconnection strategies) |
logHttpError |
- | Log HTTP errors |
logWebSocketConnectionError |
- | Log WebSocket errors |
* When validateResponseContentType is true, 2xx responses require application/graphql-response+json or application/json content type, and 4xx responses require application/graphql-response+json content type.
| Method | Description | Notes |
|---|---|---|
executeQueryRaw<TData, TVariables> |
Execute a GraphQL query | |
executeQuery<TData, TVariables> |
Execute a GraphQL query with caching | |
executeSubscription<TData, TVariables> |
Execute a GraphQL subscription | |
getPendingRequests |
Get count of pending requests | |
getActiveSubscriptions |
Get count of active subscriptions | |
refreshAll |
Refresh all cached queries | The force option cancels any in-progress requests and forces all cached queries to be refetched from the server, even if they are currently loading |
clearCache |
Clear the query cache | |
resetStore |
Reset and refresh all cached queries | Should be used anytime the user is logged in/out to refresh permissions |
Execute a GraphQL query with caching and automatic re-rendering.
Parameters:
| Parameter | Description |
|---|---|
query (required) |
GraphQL query string or typed document |
options |
Query options |
options.variables |
Query variables |
options.fetchPolicy |
Caching strategy. Options: 'cache-first', 'no-cache', 'cache-and-network' |
options.client |
Client instance or name from context |
options.guest |
Whether to use the guest client |
options.skip |
Whether to skip execution of the query |
options.autoRefetch |
Whether to automatically refetch when query/variables change |
options.operationName |
The name of the operation |
options.extensions |
Additional extensions to add to the query |
Returns:
| Property | Description |
|---|---|
data |
Query result data |
errors |
Array of GraphQL errors or undefined |
error |
The first GraphQL error object if any errors occurred, otherwise undefined |
extensions |
Additional information returned by the query |
networkError |
Indicates whether a network error occurred |
loading |
Indicates whether the query is presently loading |
refetch |
Function to refetch the query |
Execute a GraphQL mutation.
Parameters:
| Parameter | Description |
|---|---|
mutation (required) |
GraphQL mutation string or typed document |
options |
Mutation options |
options.client |
Client instance or name from context |
options.guest |
Whether to use the guest client |
options.variables |
Default variables for the mutation |
options.operationName |
The name of the operation |
options.extensions |
Additional extensions to add to the mutation |
Returns:
| Index | Description |
|---|---|
[0] |
Mutation function that returns a promise |
Execute a GraphQL subscription with manual control over when the subscription starts and stops.
Parameters:
| Parameter | Description |
|---|---|
query (required) |
GraphQL subscription string or typed document |
options |
Subscription options |
options.variables |
Subscription variables |
options.client |
Client instance or name from context |
options.guest |
Whether to use the guest client |
options.operationName |
The name of the operation |
options.extensions |
Additional extensions to add to the subscription |
options.timeoutStrategy |
Timeout strategy for the subscription |
options.onData |
Callback function to invoke when new data is received |
options.onClose |
Callback function to invoke when the subscription is closed |
options.onOpen |
Callback function to invoke when the subscription connection is opened |
Returns:
| Index | Description |
|---|---|
[0] |
Subscription function that returns { connected: Promise<void>; abort: () => void } |
Execute a GraphQL subscription with automatic lifecycle management and reconnection capabilities.
Parameters:
| Parameter | Description |
|---|---|
query (required) |
GraphQL subscription string or typed document |
options |
Auto-subscription options |
options.variables |
Subscription variables or function that returns variables |
options.client |
Client instance or name from context |
options.guest |
Whether to use the guest client |
options.operationName |
The name of the operation |
options.extensions |
Additional extensions to add to the subscription |
options.timeoutStrategy |
Timeout strategy for the subscription |
options.reconnectionStrategy |
Reconnection strategy for automatic reconnection |
options.enabled |
Whether the subscription should be enabled (default: true) |
options.onData |
Callback function to invoke when new data is received |
options.onClose |
Callback function to invoke when the subscription is closed |
options.onOpen |
Callback function to invoke when the subscription connection is opened |
Returns:
| Property | Description |
|---|---|
state |
Current state of the subscription (AutoSubscriptionState enum) |
AutoSubscriptionState Values:
| State | Description |
|---|---|
Disconnected |
The subscription is not connected |
Connecting |
The subscription is in the process of connecting or reconnecting |
Connected |
The subscription is connected and receiving data |
Error |
The subscription has failed and reconnection attempts have been exhausted |
Rejected |
The subscription was rejected by the server |
Completed |
The subscription was completed by the server |
React context for providing GraphQL clients to hooks.
Interface:
interface IGraphQLContext {
client: IGraphQLClient; // Default client
[key: string]: IGraphQLClient; // Additional named clients
}Aborts subscriptions if no inbound messages are received within the specified timeout period.
Constructor:
idleMs(number): Timeout in milliseconds
Sends periodic pings and expects matching pongs within a deadline. Aborts if pong is not received in time.
Constructor:
ackTimeoutMs(number): Connection acknowledgment timeout in millisecondspingIntervalMs(number): Interval between ping messages in millisecondspongDeadlineMs(number): Maximum time to wait for pong response in milliseconds
This package includes a GraphQLTestClient for easy testing and mocking:
import { GraphQLTestClient } from '@shane32/graphql';
describe('GraphQL Tests', () => {
let testClient: GraphQLTestClient;
beforeEach(() => {
testClient = new GraphQLTestClient();
});
it('should mock query responses', async () => {
// Mock a query response
testClient.mockQuery(
'query GetUser($id: ID!) { user(id: $id) { name email } }',
{ user: { name: 'John Doe', email: 'john@example.com' } }
);
// Execute the query
const result = await testClient.executeQueryRaw({
query: 'query GetUser($id: ID!) { user(id: $id) { name email } }',
variables: { id: '123' }
}).result;
expect(result.data.user.name).toBe('John Doe');
});
it('should mock error responses', () => {
// Mock an error response
testClient.mockError(
'query GetUser($id: ID!) { user(id: $id) { name } }',
new Error('User not found')
);
// The query will throw the mocked error
expect(() =>
testClient.executeQueryRaw({
query: 'query GetUser($id: ID!) { user(id: $id) { name } }',
variables: { id: '999' }
})
).toThrow('User not found');
});
});import { render, screen } from '@testing-library/react';
import { GraphQLContext, GraphQLTestClient } from '@shane32/graphql';
import UserProfile from './UserProfile';
test('renders user profile', async () => {
const testClient = new GraphQLTestClient();
testClient.mockQuery(
'query GetUser($id: ID!) { user(id: $id) { name email } }',
{ user: { name: 'John Doe', email: 'john@example.com' } }
);
render(
<GraphQLContext.Provider value={{ client: testClient }}>
<UserProfile userId="123" />
</GraphQLContext.Provider>
);
expect(await screen.findByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});This package requires the Fetch API. For older browsers or test environments:
// Install a polyfill
npm install whatwg-fetch
// Import in your app or test setup
import 'whatwg-fetch';- Ensure your GraphQL server supports the graphql-ws protocol
- Check that the WebSocket URL is correct and accessible
- Verify authentication if required using the
generatePayloadoption
-
Ensure you have the correct peer dependencies installed
-
Use proper TypeScript generics with hooks:
const { data } = useQuery<QueryResult, QueryVariables>(query, options);
- Use
fetchPolicy: 'no-cache'to bypass cache for testing - Clear cache manually if needed (implementation depends on your setup)
- Check
defaultCacheTimeandmaxCacheSizesettings
-
Use
transformRequestto add authentication headers:const client = new GraphQLClient({ url: 'https://api.example.com/graphql', transformRequest: async (request) => { const token = await getAuthToken(); if (!request.headers) { request.headers = {}; } request.headers['Authorization'] = `Bearer ${token}`; return request; } });
Glory to Jehovah, Lord of Lords and King of Kings, creator of Heaven and Earth, who through his Son Jesus Christ, has reedemed me to become a child of God. -Shane32