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