Executing Queries in Stack9
Learn how to execute queries in your Stack9 applications using the query service and React hooks. This guide covers the most common patterns for fetching data from your Stack9 backend.
What You'll Learn
- ✅ Using
queryService.runNamedQuery()for direct API calls - ✅ Using
useScreenQueryhook for React components - ✅ Using
useScreenQueryByIdhook for single records - ✅ Implementing pagination, search, and filtering
- ✅ Handling loading states and errors
- ✅ Cache management and data revalidation
- ✅ Best practices and performance optimization
Time Required: 20-30 minutes
Prerequisites
- Completed Custom Queries guide
- Understanding of React hooks
- Queries defined in your Stack9 Query Library
Query Execution Methods
Stack9 provides three primary ways to execute queries:
queryService.runNamedQuery()- Direct API service method (SDK)useScreenQuery()- React hook for list views and complex queries (UI)useScreenQueryById()- React hook for single record by ID (UI)
Method 1: Using queryService.runNamedQuery()
The queryService from @april9au/stack9-sdk provides direct access to execute queries. Use this when:
- You need to execute queries outside of React components
- You're in action types, entity hooks, or server-side code
- You need more control over the request lifecycle
Basic Setup
import { useStack9 } from '@april9/stack9-react';
function MyComponent() {
const { queryService } = useStack9();
// Now you can use queryService.runNamedQuery()
}
Simple Query Execution
async function loadCustomers() {
const response = await queryService.runNamedQuery(
'customer_list', // Screen key
'getcustomerlist' // Query name
);
console.log(response.data); // Array of customers
}
Query with Variables
async function loadCustomer(customerId: number) {
const response = await queryService.runNamedQuery(
'customer_detail',
'getcustomer',
{
vars: { id: customerId }
}
);
return response.data; // Single customer object
}
Query with Pagination
async function loadProductsPage(page: number, pageSize: number) {
const response = await queryService.runNamedQuery(
'product_list',
'getproductlist',
{
vars: {
page: page,
limit: pageSize
}
}
);
return response.data;
}
Query with Filters and Search
async function searchCustomers(searchTerm: string, status: string) {
const response = await queryService.runNamedQuery(
'customer_list',
'searchcustomers',
{
querySearch: searchTerm,
filters: {
status: {
key: 'status',
operator: 'equals',
value: status
}
},
sorting: {
column: 'name',
direction: 'asc'
}
}
);
return response.data;
}
Complete Example in a Component
import { useState, useEffect } from 'react';
import { useStack9 } from '@april9/stack9-react';
import { Button, Table, notification } from 'antd';
interface Customer {
id: number;
name: string;
email: string;
status: string;
}
function CustomerTable() {
const { queryService } = useStack9();
const [customers, setCustomers] = useState<Customer[]>([]);
const [loading, setLoading] = useState(false);
const loadData = async () => {
setLoading(true);
try {
const response = await queryService.runNamedQuery<Customer[]>(
'customer_list',
'getcustomerlist',
{
vars: { page: 0, limit: 50 }
}
);
setCustomers(response.data || []);
} catch (error) {
notification.error({
message: 'Failed to load customers',
description: error.message
});
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData();
}, []);
return (
<>
<Button onClick={loadData} loading={loading}>
Refresh
</Button>
<Table
dataSource={customers}
loading={loading}
rowKey="id"
columns={[
{ title: 'Name', dataIndex: 'name' },
{ title: 'Email', dataIndex: 'email' },
{ title: 'Status', dataIndex: 'status' }
]}
/>
</>
);
}
Method 2: Using useScreenQuery Hook
The useScreenQuery hook from @april9au/stack9-ui is the recommended way to execute queries in React components. It provides:
- Automatic caching with SWR
- Loading and error states
- Automatic revalidation
- Optimistic updates
Basic Usage
import * as ui from '@april9/stack9-ui';
function CustomerList() {
const { data, error, isLoading } = ui.useScreenQuery<Customer[]>(
'getcustomerlist',
{ shouldFetch: true }
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data?.map(customer => (
<li key={customer.id}>{customer.name}</li>
))}
</ul>
);
}
Query with Pagination
function PaginatedCustomerList() {
const [page, setPage] = useState(0);
const [limit] = useState(20);
const { data, isLoading } = ui.useScreenQuery<{
items: Customer[];
total: number;
}>(
'getcustomerlist',
{
vars: { page, limit }
}
);
return (
<>
<Table
dataSource={data?.items}
loading={isLoading}
pagination={{
current: page + 1,
pageSize: limit,
total: data?.total,
onChange: (newPage) => setPage(newPage - 1)
}}
/>
</>
);
}
Query with Search
function SearchableCustomerList() {
const [searchTerm, setSearchTerm] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
// Debounce search input
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearch(searchTerm);
}, 300);
return () => clearTimeout(timer);
}, [searchTerm]);
const { data, isLoading } = ui.useScreenQuery<Customer[]>(
'searchcustomers',
{
querySearch: debouncedSearch,
vars: { limit: 50 }
}
);
return (
<>
<Input.Search
placeholder="Search customers..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{ marginBottom: 16 }}
/>
<Spin spinning={isLoading}>
<List
dataSource={data}
renderItem={(customer) => (
<List.Item>
<List.Item.Meta
title={customer.name}
description={customer.email}
/>
</List.Item>
)}
/>
</Spin>
</>
);
}
Query with Filters
function FilteredCustomerList() {
const [statusFilter, setStatusFilter] = useState<string>('Active');
const { data, isLoading } = ui.useScreenQuery<Customer[]>(
'getcustomerlist',
{
vars: { page: 0, limit: 50 },
filters: {
status: {
key: 'status',
operator: 'equals',
value: statusFilter
}
}
}
);
return (
<>
<Select
value={statusFilter}
onChange={setStatusFilter}
options={[
{ label: 'All', value: '' },
{ label: 'Active', value: 'Active' },
{ label: 'Inactive', value: 'Inactive' }
]}
/>
<Table
dataSource={data}
loading={isLoading}
columns={[/* ... */]}
/>
</>
);
}
Manual Revalidation
function CustomerListWithRefresh() {
const { data, mutate, isValidating } = ui.useScreenQuery<Customer[]>(
'getcustomerlist',
{ shouldFetch: true }
);
const handleRefresh = () => {
mutate(); // Trigger revalidation
};
const handleDelete = async (id: number) => {
await deleteCustomer(id);
mutate(); // Refresh data after delete
};
return (
<>
<Button
onClick={handleRefresh}
loading={isValidating}
icon={<ReloadOutlined />}
>
Refresh
</Button>
<CustomerList
customers={data}
onDelete={handleDelete}
/>
</>
);
}
Method 3: Using useScreenQueryById Hook
The useScreenQueryById hook is a specialized version for fetching single records by ID.
Basic Usage
import * as ui from '@april9/stack9-ui';
function CustomerDetail({ customerId }: { customerId: number }) {
const { data: customer, error, isLoading } = ui.useScreenQueryById<Customer>(
'getcustomer',
customerId
);
if (isLoading) return <Skeleton active />;
if (error) return <Alert type="error" message="Failed to load customer" />;
if (!customer) return <Empty description="Customer not found" />;
return (
<Card title={customer.name}>
<p>Email: {customer.email}</p>
<p>Status: {customer.status}</p>
</Card>
);
}
With Route Parameters
import { useParams } from 'react-router-dom';
function CustomerDetailPage() {
const { id } = useParams<{ id: string }>();
const customerId = id ? parseInt(id) : undefined;
const { data: customer, error } = ui.useScreenQueryById<Customer>(
'getcustomer',
customerId
);
if (!customerId) {
return <Navigate to="/customers" />;
}
if (error) {
return <ErrorPage error={error} />;
}
return <CustomerDetailView customer={customer} />;
}
Conditional Loading
function EditCustomerModal({
visible,
customerId
}: {
visible: boolean;
customerId?: number;
}) {
// Only fetch when modal is visible and customerId exists
const { data: customer } = ui.useScreenQueryById<Customer>(
'getcustomer',
visible ? customerId : undefined
);
return (
<Modal visible={visible}>
<Form initialValues={customer}>
{/* Form fields */}
</Form>
</Modal>
);
}
Advanced Patterns
Pattern 1: Master-Detail View
function CustomerMasterDetail() {
const [selectedCustomerId, setSelectedCustomerId] = useState<number>();
const { data: customers } = ui.useScreenQuery<Customer[]>(
'getcustomerlist',
{ shouldFetch: true }
);
const { data: customerDetail } = ui.useScreenQueryById<CustomerDetail>(
'getcustomerdetail',
selectedCustomerId
);
return (
<Row gutter={16}>
<Col span={8}>
<List
dataSource={customers}
renderItem={(customer) => (
<List.Item
onClick={() => setSelectedCustomerId(customer.id)}
style={{ cursor: 'pointer' }}
>
{customer.name}
</List.Item>
)}
/>
</Col>
<Col span={16}>
{customerDetail && (
<CustomerDetailPanel customer={customerDetail} />
)}
</Col>
</Row>
);
}
Pattern 2: Related Data Loading
function CustomerWithOrders({ customerId }: { customerId: number }) {
// Load customer
const { data: customer } = ui.useScreenQueryById<Customer>(
'getcustomer',
customerId
);
// Load customer's orders
const { data: orders } = ui.useScreenQuery<Order[]>(
'getcustomerorders',
{
shouldFetch: !!customerId,
vars: { customerId }
}
);
// Load customer's invoices
const { data: invoices } = ui.useScreenQuery<Invoice[]>(
'getcustomerinvoices',
{
shouldFetch: !!customerId,
vars: { customerId }
}
);
return (
<>
<CustomerHeader customer={customer} />
<Tabs>
<Tabs.TabPane key="orders" tab="Orders">
<OrdersList orders={orders} />
</Tabs.TabPane>
<Tabs.TabPane key="invoices" tab="Invoices">
<InvoicesList invoices={invoices} />
</Tabs.TabPane>
</Tabs>
</>
);
}
Pattern 3: Optimistic Updates
function CustomerStatusToggle({ customerId }: { customerId: number }) {
const { data: customer, mutate } = ui.useScreenQueryById<Customer>(
'getcustomer',
customerId
);
const handleToggleStatus = async () => {
const newStatus = customer.status === 'Active' ? 'Inactive' : 'Active';
// Optimistically update UI
mutate(
{ ...customer, status: newStatus },
false // Don't revalidate yet
);
try {
// Update on server
await updateCustomerStatus(customerId, newStatus);
// Revalidate to get server state
mutate();
notification.success({
message: 'Status updated successfully'
});
} catch (error) {
// Revert on error
mutate();
notification.error({
message: 'Failed to update status'
});
}
};
return (
<Switch
checked={customer?.status === 'Active'}
onChange={handleToggleStatus}
/>
);
}
Pattern 4: Infinite Scroll
function InfiniteCustomerList() {
const [page, setPage] = useState(0);
const [allCustomers, setAllCustomers] = useState<Customer[]>([]);
const { data, isLoading } = ui.useScreenQuery<Customer[]>(
'getcustomerlist',
{
vars: { page, limit: 20 }
}
);
useEffect(() => {
if (data) {
setAllCustomers(prev => [...prev, ...data]);
}
}, [data]);
const loadMore = () => {
if (!isLoading) {
setPage(prev => prev + 1);
}
};
return (
<div>
<List
dataSource={allCustomers}
renderItem={(customer) => (
<List.Item>{customer.name}</List.Item>
)}
/>
<Button
onClick={loadMore}
loading={isLoading}
block
>
Load More
</Button>
</div>
);
}
Pattern 5: Dependent Queries
function OrderDetailsWithProduct() {
const { id: orderId } = useParams<{ id: string }>();
// First query: Get order
const { data: order } = ui.useScreenQueryById<Order>(
'getorder',
orderId ? parseInt(orderId) : undefined
);
// Second query: Get product details (only after order loads)
const { data: product } = ui.useScreenQueryById<Product>(
'getproduct',
order?.product_id // Only fetch when we have product_id
);
return (
<>
<OrderInfo order={order} />
<ProductDetails product={product} />
</>
);
}
Error Handling
Using queryService
async function loadCustomers() {
try {
const response = await queryService.runNamedQuery(
'customer_list',
'getcustomerlist'
);
return response.data;
} catch (error) {
if (error.response?.status === 404) {
console.error('Query not found');
} else if (error.response?.status === 500) {
console.error('Server error');
} else {
console.error('Unknown error:', error.message);
}
throw error;
}
}
Using Hooks
function CustomerList() {
const { data, error, isLoading } = ui.useScreenQuery<Customer[]>(
'getcustomerlist',
{ shouldFetch: true }
);
if (error) {
// Log error to monitoring service
logError('CustomerList', error);
return (
<Alert
type="error"
message="Failed to Load Customers"
description={error.message}
action={
<Button onClick={() => window.location.reload()}>
Retry
</Button>
}
/>
);
}
if (isLoading) {
return <Skeleton active />;
}
return <CustomerTable data={data} />;
}
Performance Optimization
1. Conditional Fetching
Don't fetch data until it's needed:
function CustomerOrders({ customerId }: { customerId?: number }) {
const [showOrders, setShowOrders] = useState(false);
const { data: orders } = ui.useScreenQuery<Order[]>(
'getorders',
{
shouldFetch: showOrders && !!customerId,
vars: { customerId }
}
);
return (
<>
<Button onClick={() => setShowOrders(true)}>
Show Orders
</Button>
{showOrders && <OrderList orders={orders} />}
</>
);
}
2. Debouncing Search
Reduce API calls during search:
import { useDebouncedValue } from '@april9/stack9-react';
function SearchableList() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearch = useDebouncedValue(searchTerm, 300);
const { data } = ui.useScreenQuery<Customer[]>(
'searchcustomers',
{
querySearch: debouncedSearch
}
);
return (
<>
<Input.Search onChange={(e) => setSearchTerm(e.target.value)} />
<List dataSource={data} />
</>
);
}
3. Pagination Over Large Lists
Always paginate large datasets:
const { data } = ui.useScreenQuery<Customer[]>(
'getcustomerlist',
{
vars: {
page: currentPage,
limit: 50 // Don't fetch more than needed
}
}
);
4. Cache Revalidation Control
// Disable revalidation for static data
const { data } = ui.useScreenQuery<Category[]>(
'getcategories',
{
shouldFetch: true
}
);
// Note: revalidateOnFocus is already disabled by default
Best Practices
1. Type Your Data
Always specify TypeScript types:
interface Customer {
id: number;
name: string;
email: string;
}
const { data } = ui.useScreenQuery<Customer[]>(
'getcustomerlist',
{ shouldFetch: true }
);
// data is now typed as Customer[] | undefined
2. Handle All States
Always handle loading, error, and empty states:
function CustomerList() {
const { data, error, isLoading } = ui.useScreenQuery<Customer[]>(
'getcustomerlist',
{ shouldFetch: true }
);
if (isLoading) return <Skeleton active />;
if (error) return <ErrorMessage error={error} />;
if (!data || data.length === 0) return <Empty />;
return <Table dataSource={data} />;
}
3. Use Appropriate Hook
useScreenQueryByIdfor single recordsuseScreenQueryfor lists and complex queriesqueryService.runNamedQuery()for non-React contexts
4. Leverage SWR Features
const { data, mutate, isValidating } = ui.useScreenQuery(
'getcustomerlist',
{ shouldFetch: true }
);
// Manual revalidation
const handleRefresh = () => mutate();
// Optimistic update
const handleUpdate = (updated: Customer) => {
mutate(
data?.map(c => c.id === updated.id ? updated : c),
false
);
};
5. Centralize Query Names
// queries.ts
export const QUERIES = {
CUSTOMER_LIST: 'getcustomerlist',
CUSTOMER_DETAIL: 'getcustomer',
ORDER_LIST: 'getorderlist',
} as const;
// component.tsx
const { data } = ui.useScreenQuery(
QUERIES.CUSTOMER_LIST,
{ shouldFetch: true }
);
Comparison Table
| Feature | queryService.runNamedQuery() | useScreenQuery | useScreenQueryById |
|---|---|---|---|
| Context | Any (React or non-React) | React components only | React components only |
| Caching | None (manual) | Automatic (SWR) | Automatic (SWR) |
| Loading state | Manual | Built-in | Built-in |
| Revalidation | Manual | Automatic | Automatic |
| Use case | Actions, hooks, server code | Lists, search, filters | Single record by ID |
| Complexity | More code | Less code | Least code |
| Flexibility | High | High | Medium |
Troubleshooting
Query Returns No Data
Problem: Hook or query returns empty array or null
Solutions:
- Check query name matches exactly (case-sensitive)
- Verify screen key is correct
- Check query exists in screen's queries array
- Test query directly via API
- Check
shouldFetchis not false
Query Not Updating
Problem: Data doesn't refresh after changes
Solutions:
- Call
mutate()after mutations - Check SWR cache configuration
- Verify query dependencies in key
Performance Issues
Problem: Too many API calls or slow responses
Solutions:
- Implement pagination with
limitparameter - Use debouncing for search inputs
- Set
shouldFetch: falseuntil needed - Reduce selected fields in query definition
Next Steps
Now that you've mastered query execution:
- Building a CRUD Application - Build complete features
- Implementing Search - Advanced search patterns
- Performance Optimization - Optimize query performance
- Custom UI Development - Build custom components
Summary
You now understand:
✅ Three methods to execute queries in Stack9 ✅ When to use each method ✅ Pagination, search, and filtering patterns ✅ Error handling and loading states ✅ Performance optimization techniques ✅ Advanced patterns like optimistic updates ✅ Best practices for query execution
Query execution is fundamental to building data-driven Stack9 applications!