Calling API
Once you've defined your custom controller in the api.server.js
file, you can call those functions from your frontend component using the useCallProcedure
hook.
Basic usage
The useCallProcedure
hook provides a simple way to call your backend procedures from React components:
import { useCallProcedure, Page } from '@kottster/react';
export default () => {
const callProcedure = useCallProcedure();
const handleClick = async () => {
// Call the backend procedure
const result = await callProcedure('getProducts');
console.log(result);
};
return (
<Page>
<button onClick={handleClick}>
Load Products
</button>
</Page>
);
};
Passing parameters
You can pass parameters to your backend procedures as the second argument:
import { useState } from 'react';
import { useCallProcedure, Page } from '@kottster/react';
export default () => {
const callProcedure = useCallProcedure();
const [product, setProduct] = useState(null);
const loadProduct = async (productId) => {
const result = await callProcedure('getProduct', { id: productId });
setProduct(result);
};
return (
<Page>
<button onClick={() => loadProduct(1)}>
Load Product 1
</button>
{product && (
<div>
<h3>{product.name}</h3>
<p>Price: ${product.price}</p>
</div>
)}
</Page>
);
};
Error handling
Always wrap your API calls in try-catch blocks to handle potential errors:
import { useState } from 'react';
import { useCallProcedure, Page } from '@kottster/react';
export default () => {
const callProcedure = useCallProcedure();
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [products, setProducts] = useState([]);
const loadProducts = async () => {
setLoading(true);
setError(null);
try {
const result = await callProcedure('getProducts');
setProducts(result);
} catch (err) {
setError('Failed to load products');
console.error('Error loading products:', err);
} finally {
setLoading(false);
}
};
return (
<Page>
<button onClick={loadProducts} disabled={loading}>
{loading ? 'Loading...' : 'Load Products'}
</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
<ul>
{products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</Page>
);
};
Using with useEffect
For data that needs to be loaded when the component mounts, use useEffect
:
import { useEffect, useState } from 'react';
import { useCallProcedure, Page } from '@kottster/react';
export default () => {
const callProcedure = useCallProcedure();
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadProducts = async () => {
try {
const result = await callProcedure('getProducts');
setProducts(result);
} catch (error) {
console.error('Error loading products:', error);
} finally {
setLoading(false);
}
};
loadProducts();
}, []); // Empty dependency array means this runs once on mount
if (loading) {
return <Page><div>Loading...</div></Page>;
}
return (
<Page title="Products">
<ul>
{products.map(product => (
<li key={product.id}>{product.name} - ${product.price}</li>
))}
</ul>
</Page>
);
};
Type-safe calls
For TypeScript projects, import the Procedures
type from your API server to get full type safety:
import { useEffect, useState } from 'react';
import { useCallProcedure, Page } from '@kottster/react';
// Import the Procedures type from your API server file
import { type Procedures } from './api.server';
interface Product {
id: number;
name: string;
price: number;
}
export default () => {
// Get fully typed procedure calls
const callProcedure = useCallProcedure<Procedures>();
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadProducts = async () => {
try {
// TypeScript will validate function names and parameters
const result = await callProcedure('getProducts');
setProducts(result);
} catch (error) {
console.error('Error loading products:', error);
} finally {
setLoading(false);
}
};
loadProducts();
}, []);
const createProduct = async () => {
try {
// TypeScript will enforce the correct parameter structure
const newProduct = await callProcedure('createProduct', {
name: 'New Product',
price: 99.99
});
setProducts([...products, newProduct]);
} catch (error) {
console.error('Error creating product:', error);
}
};
if (loading) {
return <Page><div>Loading...</div></Page>;
}
return (
<Page title="Products">
<button onClick={createProduct}>Add Product</button>
<ul>
{products.map(product => (
<li key={product.id}>{product.name} - ${product.price}</li>
))}
</ul>
</Page>
);
};
Using with TanStack React Query
TanStack React Query provides powerful data fetching capabilities like caching, background updates, and optimistic updates. You can easily integrate it with useCallProcedure
:
Basic query
import { useQuery } from '@tanstack/react-query';
import { useCallProcedure, Page } from '@kottster/react';
import { type Procedures } from './api.server';
export default () => {
const callProcedure = useCallProcedure<Procedures>();
const { data: products, isLoading, error } = useQuery({
queryKey: ['products'],
queryFn: () => callProcedure('getProducts'),
});
if (isLoading) return <Page><div>Loading products...</div></Page>;
if (error) return <Page><div>Error: {error.message}</div></Page>;
return (
<Page title="Products">
<ul>
{products?.map(product => (
<li key={product.id}>{product.name} - ${product.price}</li>
))}
</ul>
</Page>
);
};
Query with parameters
For queries that depend on parameters, use the parameter as part of the query key:
import { useQuery } from '@tanstack/react-query';
import { useCallProcedure, usePage, Page } from '@kottster/react';
import { type Procedures } from './api.server';
export default () => {
const callProcedure = useCallProcedure<Procedures>();
const { pageId } = usePage();
const { data: product, isLoading } = useQuery({
queryKey: ['product', pageId],
queryFn: () => callProcedure('getProduct', { id: parseInt(pageId) }),
enabled: !!pageId, // Only run query if pageId exists
});
if (isLoading) return <div>Loading...</div>;
return (
<Page>
<h1>{product?.name}</h1>
<p>Price: ${product?.price}</p>
</Page>
);
};
Mutations
Use useMutation
for create, update, and delete operations:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useCallProcedure, Page } from '@kottster/react';
import { type Procedures } from './api.server';
export default () => {
const callProcedure = useCallProcedure<Procedures>();
const queryClient = useQueryClient();
const { data: products, isLoading } = useQuery({
queryKey: ['products'],
queryFn: () => callProcedure('getProducts'),
});
const createProductMutation = useMutation({
mutationFn: (newProduct: { name: string; price: number }) =>
callProcedure('createProduct', newProduct),
onSuccess: () => {
// Invalidate and refetch products after successful creation
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
const deleteProductMutation = useMutation({
mutationFn: (productId: number) =>
callProcedure('deleteProduct', { id: productId }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
const handleCreateProduct = () => {
createProductMutation.mutate({
name: 'New Product',
price: 99.99,
});
};
const handleDeleteProduct = (productId: number) => {
deleteProductMutation.mutate(productId);
};
if (isLoading) return <Page><div>Loading...</div></Page>;
return (
<Page title="Products">
<button
onClick={handleCreateProduct}
disabled={createProductMutation.isPending}
>
{createProductMutation.isPending ? 'Creating...' : 'Add Product'}
</button>
<ul>
{products?.map(product => (
<li key={product.id}>
{product.name} - ${product.price}
<button
onClick={() => handleDeleteProduct(product.id)}
disabled={deleteProductMutation.isPending}
>
Delete
</button>
</li>
))}
</ul>
</Page>
);
};
React Query automatically handles caching, background refetching, and error recovery, making your custom pages more robust and performant.
The useCallProcedure
hook automatically handles the connection to your custom controller and provides a clean, type-safe interface for calling your backend procedures.