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
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
API Client Guide Learn more about ApiClient configuration
State Management Explore QueryClient for advanced caching
TypeScript Support Generate types from OpenAPI specs
Error Handling Best practices for error handling