@shane32/graphql

January 12, 2026 ยท View on GitHub

A TypeScript-first GraphQL client for React applications with built-in caching, subscriptions, and testing utilities.

NPM Version License: MIT

Features

  • ๐Ÿš€ 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 - useQuery and useMutation hooks for seamless React integration
  • ๐Ÿ”ง Flexible configuration - Customizable request transformation and error handling

Table of Contents

Installation

# npm
npm install @shane32/graphql

# yarn
yarn add @shane32/graphql

# pnpm
pnpm add @shane32/graphql

Peer Dependencies

This package requires the following peer dependencies:

  • react >= 16
  • react-dom >= 16
  • graphql >= 16 (optional, only needed for testing via GraphQLTestClient)

Requirements

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.

Quick Start

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>
  );
}

Advanced Configuration

  1. 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
        });
    }
});
  1. Set up context for hooks
const context: IGraphQLContext = {
    client: client,                     // required: default client
    guest: guestClient,                 // optional: guest client
    "alt": altClient                    // optional: any other clients
};
  1. Provide context to application
<GraphQLContext.Provider value={context}>
    ...
</GraphQLContext.Provider>

Usage

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.

Direct use - query/mutation/subscription

// 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 */ }
);

useQuery hook

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>
    );
};

useMutation hook

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>
    );
};

useSubscription hook

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>
    );
};

useAutoSubscription hook

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>
    );
};

Subscription Timeout Strategies

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")
});

Subscription Reconnection Strategies

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

GraphQL Codegen Support

If you want to add GraphQL Codegen to your project, refer to CODEGEN-README.md

GraphQL Tag Functions

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 { ... }`;

Creating Request Objects

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.

API Reference

GraphQLClient

Constructor Options

ParameterDefaultDescription
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'
defaultCacheTime86400000Cache duration in milliseconds (24 hours)
maxCacheSize20971520Maximum cache size in bytes (20MB)
asFormfalseUse form data instead of JSON for requests
sendDocumentIdAsQueryfalseInclude documentId as query parameter instead of POST body
validateResponseContentTypefalseValidate 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.

Methods

MethodDescriptionNotes
executeQueryRaw<TData, TVariables>Execute a GraphQL query
executeQuery<TData, TVariables>Execute a GraphQL query with caching
executeSubscription<TData, TVariables>Execute a GraphQL subscription
getPendingRequestsGet count of pending requests
getActiveSubscriptionsGet count of active subscriptions
refreshAllRefresh all cached queriesThe force option cancels any in-progress requests and forces all cached queries to be refetched from the server, even if they are currently loading
clearCacheClear the query cache
resetStoreReset and refresh all cached queriesShould be used anytime the user is logged in/out to refresh permissions

React Hooks

useQuery<TData, TVariables>

Execute a GraphQL query with caching and automatic re-rendering.

Parameters:

ParameterDescription
query (required)GraphQL query string or typed document
optionsQuery options
options.variablesQuery variables
options.fetchPolicyCaching strategy. Options: 'cache-first', 'no-cache', 'cache-and-network'
options.clientClient instance or name from context
options.guestWhether to use the guest client
options.skipWhether to skip execution of the query
options.autoRefetchWhether to automatically refetch when query/variables change
options.operationNameThe name of the operation
options.extensionsAdditional extensions to add to the query

Returns:

PropertyDescription
dataQuery result data
errorsArray of GraphQL errors or undefined
errorThe first GraphQL error object if any errors occurred, otherwise undefined
extensionsAdditional information returned by the query
networkErrorIndicates whether a network error occurred
loadingIndicates whether the query is presently loading
refetchFunction to refetch the query

useMutation<TData, TVariables>

Execute a GraphQL mutation.

Parameters:

ParameterDescription
mutation (required)GraphQL mutation string or typed document
optionsMutation options
options.clientClient instance or name from context
options.guestWhether to use the guest client
options.variablesDefault variables for the mutation
options.operationNameThe name of the operation
options.extensionsAdditional extensions to add to the mutation

Returns:

IndexDescription
[0]Mutation function that returns a promise

useSubscription<TData, TVariables>

Execute a GraphQL subscription with manual control over when the subscription starts and stops.

Parameters:

ParameterDescription
query (required)GraphQL subscription string or typed document
optionsSubscription options
options.variablesSubscription variables
options.clientClient instance or name from context
options.guestWhether to use the guest client
options.operationNameThe name of the operation
options.extensionsAdditional extensions to add to the subscription
options.timeoutStrategyTimeout strategy for the subscription
options.onDataCallback function to invoke when new data is received
options.onCloseCallback function to invoke when the subscription is closed
options.onOpenCallback function to invoke when the subscription connection is opened

Returns:

IndexDescription
[0]Subscription function that returns { connected: Promise<void>; abort: () => void }

useAutoSubscription<TData, TVariables>

Execute a GraphQL subscription with automatic lifecycle management and reconnection capabilities.

Parameters:

ParameterDescription
query (required)GraphQL subscription string or typed document
optionsAuto-subscription options
options.variablesSubscription variables or function that returns variables
options.clientClient instance or name from context
options.guestWhether to use the guest client
options.operationNameThe name of the operation
options.extensionsAdditional extensions to add to the subscription
options.timeoutStrategyTimeout strategy for the subscription
options.reconnectionStrategyReconnection strategy for automatic reconnection
options.enabledWhether the subscription should be enabled (default: true)
options.onDataCallback function to invoke when new data is received
options.onCloseCallback function to invoke when the subscription is closed
options.onOpenCallback function to invoke when the subscription connection is opened

Returns:

PropertyDescription
stateCurrent state of the subscription (AutoSubscriptionState enum)

AutoSubscriptionState Values:

StateDescription
DisconnectedThe subscription is not connected
ConnectingThe subscription is in the process of connecting or reconnecting
ConnectedThe subscription is connected and receiving data
ErrorThe subscription has failed and reconnection attempts have been exhausted
RejectedThe subscription was rejected by the server
CompletedThe subscription was completed by the server

Context

GraphQLContext

React context for providing GraphQL clients to hooks.

Interface:

interface IGraphQLContext {
  client: IGraphQLClient;        // Default client
  [key: string]: IGraphQLClient; // Additional named clients
}

Timeout Strategies

IdleTimeoutStrategy

Aborts subscriptions if no inbound messages are received within the specified timeout period.

Constructor:

  • idleMs (number): Timeout in milliseconds

CorrelatedPingStrategy

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 milliseconds
  • pingIntervalMs (number): Interval between ping messages in milliseconds
  • pongDeadlineMs (number): Maximum time to wait for pong response in milliseconds

Testing

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');
  });
});

Testing with React Components

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();
});

Troubleshooting

Common Issues

Fetch API not available

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';

WebSocket connection fails

  • Ensure your GraphQL server supports the graphql-ws protocol
  • Check that the WebSocket URL is correct and accessible
  • Verify authentication if required using the generatePayload option

TypeScript errors with queries

  • Ensure you have the correct peer dependencies installed

  • Use proper TypeScript generics with hooks:

    const { data } = useQuery<QueryResult, QueryVariables>(query, options);
    

Caching issues

  • Use fetchPolicy: 'no-cache' to bypass cache for testing
  • Clear cache manually if needed (implementation depends on your setup)
  • Check defaultCacheTime and maxCacheSize settings

Authentication problems

  • Use transformRequest to 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;
      }
    });
    

Credits

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