Skip to main content

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 useScreenQuery hook for React components
  • ✅ Using useScreenQueryById hook 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:

  1. queryService.runNamedQuery() - Direct API service method (SDK)
  2. useScreenQuery() - React hook for list views and complex queries (UI)
  3. 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;
}
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)
}}
/>
</>
);
}
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>
);
}
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} />}
</>
);
}

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

  • useScreenQueryById for single records
  • useScreenQuery for lists and complex queries
  • queryService.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

FeaturequeryService.runNamedQuery()useScreenQueryuseScreenQueryById
ContextAny (React or non-React)React components onlyReact components only
CachingNone (manual)Automatic (SWR)Automatic (SWR)
Loading stateManualBuilt-inBuilt-in
RevalidationManualAutomaticAutomatic
Use caseActions, hooks, server codeLists, search, filtersSingle record by ID
ComplexityMore codeLess codeLeast code
FlexibilityHighHighMedium

Troubleshooting

Query Returns No Data

Problem: Hook or query returns empty array or null

Solutions:

  1. Check query name matches exactly (case-sensitive)
  2. Verify screen key is correct
  3. Check query exists in screen's queries array
  4. Test query directly via API
  5. Check shouldFetch is not false

Query Not Updating

Problem: Data doesn't refresh after changes

Solutions:

  1. Call mutate() after mutations
  2. Check SWR cache configuration
  3. Verify query dependencies in key

Performance Issues

Problem: Too many API calls or slow responses

Solutions:

  1. Implement pagination with limit parameter
  2. Use debouncing for search inputs
  3. Set shouldFetch: false until needed
  4. Reduce selected fields in query definition

Next Steps

Now that you've mastered query execution:

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!