Skip to main content

Overview

ByteKit integrates seamlessly with React applications through custom hooks that wrap the ApiClient and QueryClient. This guide shows you how to build type-safe, performant API integrations in React.

Installation

npm install bytekit

Basic Setup

Create ApiClient Hook

Create a custom hook to initialize and share the ApiClient instance:
import { useState } from "react";
import { createApiClient } from "bytekit";

function useApiClient(config: Parameters<typeof createApiClient>[0]) {
    const [client] = useState(() => createApiClient(config));
    return client;
}
The useState initializer function ensures the client is created only once per component lifecycle.

Create Query Hook

Build a reusable hook for fetching data:
import { useState, useEffect } from "react";
import { createApiClient } from "bytekit";

function useApiQuery<T>(
    client: ReturnType<typeof createApiClient>,
    url: string
) {
    const [data, setData] = useState<T | null>(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState<string | null>(null);

    useEffect(() => {
        let cancelled = false;

        async function fetchData() {
            try {
                setLoading(true);
                const response = await client.get(url);
                if (!cancelled) {
                    setData(response as T);
                    setError(null);
                }
            } catch (err) {
                if (!cancelled) {
                    setError(
                        err instanceof Error ? err.message : "Unknown error"
                    );
                }
            } finally {
                if (!cancelled) {
                    setLoading(false);
                }
            }
        }

        fetchData();

        return () => {
            cancelled = true;
        };
    }, [client, url]);

    return { data, loading, error };
}

Complete Example

import { useState, useEffect } from "react";
import { createApiClient } from "bytekit";

interface User {
    id: number;
    name: string;
    email: string;
    phone: string;
    website: string;
}

export default function App() {
    const client = useApiClient({
        baseUrl: "https://api.example.com",
        timeoutMs: 5000,
        retryPolicy: { maxRetries: 3 },
    });

    const { data, loading, error } = useApiQuery<User>(client, "/users/1");

    return (
        <div style={{ padding: "2rem", fontFamily: "system-ui" }}>
            <h1>ByteKit + React</h1>

            <div style={{ marginTop: "2rem" }}>
                <h2>API Client Example</h2>

                {loading && <p>Loading...</p>}
                {error && <p style={{ color: "red" }}>Error: {error}</p>}
                {data && (
                    <pre
                        style={{
                            background: "#f5f5f5",
                            padding: "1rem",
                            borderRadius: "8px",
                            overflow: "auto",
                        }}
                    >
                        {JSON.stringify(data, null, 2)}
                    </pre>
                )}
            </div>
        </div>
    );
}

Using QueryClient

For advanced state management, use ByteKit’s QueryClient with React:
import { useState, useEffect } from "react";
import { createApiClient, createQueryClient } from "bytekit";

const apiClient = createApiClient({
    baseUrl: "https://api.example.com",
});

const queryClient = createQueryClient(apiClient, {
    defaultStaleTime: 5000,
    defaultCacheTime: 60000,
});

function useQuery<T>(queryKey: string[], path: string) {
    const [data, setData] = useState<T | null>(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState<Error | null>(null);

    useEffect(() => {
        let cancelled = false;

        async function fetch() {
            try {
                setLoading(true);
                const result = await queryClient.query({
                    queryKey,
                    path,
                });
                if (!cancelled) {
                    setData(result as T);
                    setError(null);
                }
            } catch (err) {
                if (!cancelled) {
                    setError(err as Error);
                }
            } finally {
                if (!cancelled) {
                    setLoading(false);
                }
            }
        }

        fetch();

        return () => {
            cancelled = true;
        };
    }, [queryKey.join(","), path]);

    return { data, loading, error };
}

// Usage
function UserProfile({ userId }: { userId: string }) {
    const { data, loading, error } = useQuery<User>(
        ["user", userId],
        `/users/${userId}`
    );

    if (loading) return <div>Loading...</div>;
    if (error) return <div>Error: {error.message}</div>;
    if (!data) return null;

    return (
        <div>
            <h2>{data.name}</h2>
            <p>{data.email}</p>
        </div>
    );
}

State Management Patterns

Context Provider Pattern

Share the API client across your app using React Context:
import { createContext, useContext, ReactNode } from "react";
import { createApiClient } from "bytekit";

const ApiContext = createContext<ReturnType<typeof createApiClient> | null>(
    null
);

export function ApiProvider({ children }: { children: ReactNode }) {
    const client = createApiClient({
        baseUrl: "https://api.example.com",
        timeoutMs: 5000,
        retryPolicy: { maxRetries: 3 },
    });

    return <ApiContext.Provider value={client}>{children}</ApiContext.Provider>;
}

export function useApi() {
    const context = useContext(ApiContext);
    if (!context) {
        throw new Error("useApi must be used within ApiProvider");
    }
    return context;
}

Mutations Hook

Handle POST, PUT, DELETE operations:
import { useState } from "react";

function useMutation<T, V>(client: ReturnType<typeof createApiClient>) {
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState<string | null>(null);

    const mutate = async (url: string, data: V, method = "POST") => {
        setLoading(true);
        setError(null);

        try {
            const response = await client.request({
                url,
                method,
                body: data,
            });
            setLoading(false);
            return response as T;
        } catch (err) {
            const errorMsg =
                err instanceof Error ? err.message : "Unknown error";
            setError(errorMsg);
            setLoading(false);
            throw err;
        }
    };

    return { mutate, loading, error };
}

// Usage
function CreateUser() {
    const client = useApi();
    const { mutate, loading, error } = useMutation<User, Partial<User>>(client);

    const handleSubmit = async (formData: Partial<User>) => {
        try {
            const newUser = await mutate("/users", formData);
            console.log("Created:", newUser);
        } catch (err) {
            // Error is already set in state
        }
    };

    return (
        <form onSubmit={(e) => {
            e.preventDefault();
            const formData = { /* ... */ };
            handleSubmit(formData);
        }}>
            {/* Form fields */}
            {error && <p style={{ color: "red" }}>{error}</p>}
            <button disabled={loading}>
                {loading ? "Creating..." : "Create User"}
            </button>
        </form>
    );
}

Best Practices

Always create the ApiClient instance using useState or useMemo to prevent recreating it on every render:
const client = useState(() => createApiClient(config))[0];
// or
const client = useMemo(() => createApiClient(config), []);
Always return a cleanup function to cancel pending requests:
useEffect(() => {
    let cancelled = false;
    // ... fetch logic
    return () => {
        cancelled = true;
    };
}, [dependencies]);
Use TypeScript generics for type-safe API responses:
interface User {
    id: number;
    name: string;
}

const { data } = useApiQuery<User>(client, "/users/1");
// data is typed as User | null
Wrap components in error boundaries to catch rendering errors:
import { ErrorBoundary } from "react-error-boundary";

<ErrorBoundary fallback={<div>Something went wrong</div>}>
    <UserProfile userId="1" />
</ErrorBoundary>

Next Steps