Skip to main content

Stack9 UI Package

The @april9au/stack9-ui package provides a comprehensive set of UI components, layouts, and utilities for building Stack9 applications. It includes form controls, data displays, navigation elements, and complete page templates built on top of Ant Design.

Installation​

npm install @april9au/stack9-ui

Package Structure​

@april9au/stack9-ui
├── components/
│ ├── common/ # Common reusable components
│ ├── layout/ # Layout components
│ └── native/ # Stack9-specific components
├── pages/ # Complete page templates
├── contexts/ # React contexts
├── hooks/ # UI-specific hooks
├── providers/ # UI providers
├── constants/ # Constants and configurations
└── utils/ # Utility functions

Core Setup​

UIProvider​

The root provider that registers all UI components and extensions.

import { UIProvider } from '@april9au/stack9-ui';
import { components } from './components';
import { extensions } from './extensions';

function App() {
return (
<UIProvider
components={components}
extensions={extensions}
>
{/* Your app */}
</UIProvider>
);
}

Props:

  • components: Component[] - Array of registered components
  • extensions: object - Custom component extensions

App Component​

The main application wrapper that handles routing and layout.

import { App } from '@april9au/stack9-ui';

function MyApp() {
return (
<App
customRoutes={[
{ route: '/custom', component: <CustomPage /> }
]}
onRouteChange={(route) => console.log('Route:', route)}
/>
);
}

Props:

  • customRoutes: RouteConfig[] - Custom route definitions
  • onRouteChange: (route: string) => void - Route change callback
  • theme: ThemeConfig - Theme customization

Layout Components​

S9Layout​

Main layout wrapper with sidebar navigation.

import { S9Layout, LayoutHeader, LayoutSider, LayoutContent } from '@april9au/stack9-ui';

function AppLayout() {
return (
<S9Layout>
<LayoutHeader>
<Logo />
<UserMenu />
</LayoutHeader>
<S9Layout>
<LayoutSider width={240} collapsible>
<Navigation />
</LayoutSider>
<LayoutContent>
<Routes />
</LayoutContent>
</S9Layout>
</S9Layout>
);
}

S9Page​

Standard page wrapper with title and breadcrumbs.

import { S9Page, S9PageTitle, S9PageHeader, S9PageContent } from '@april9au/stack9-ui';

function CustomerPage() {
return (
<S9Page>
<S9PageHeader
title="Customers"
breadcrumb={[
{ label: 'Home', path: '/' },
{ label: 'Customers' }
]}
actions={[
<S9Button type="primary">Add Customer</S9Button>
]}
/>
<S9PageContent>
<CustomerList />
</S9PageContent>
</S9Page>
);
}

S9PageHeader Props:

  • title: string - Page title
  • subtitle?: string - Page subtitle
  • breadcrumb?: BreadcrumbItem[] - Breadcrumb navigation
  • actions?: ReactNode[] - Action buttons
  • extra?: ReactNode - Additional content

S9Grid System​

Responsive grid layout system based on 24 columns.

import { S9Row, S9Col } from '@april9au/stack9-ui';

function GridLayout() {
return (
<S9Row gutter={[16, 16]}>
<S9Col xs={24} sm={12} md={8} lg={6}>
<Card>Column 1</Card>
</S9Col>
<S9Col xs={24} sm={12} md={8} lg={6}>
<Card>Column 2</Card>
</S9Col>
<S9Col xs={24} sm={12} md={8} lg={12}>
<Card>Column 3</Card>
</S9Col>
</S9Row>
);
}

S9Row Props:

  • gutter?: [horizontal, vertical] - Spacing between columns
  • align?: 'top' | 'middle' | 'bottom' - Vertical alignment
  • justify?: 'start' | 'center' | 'end' | 'space-around' | 'space-between'

S9Col Props:

  • span?: number - Column width (1-24)
  • offset?: number - Column offset
  • xs, sm, md, lg, xl, xxl?: number | ColConfig - Responsive breakpoints

Form Components​

S9Form​

Enhanced form component with validation and layout.

import { S9Form, S9TextField, S9NumericField, S9Button } from '@april9au/stack9-ui';

function CustomerForm() {
const [form] = S9Form.useForm();

const handleSubmit = async (values) => {
await saveCustomer(values);
};

return (
<S9Form
form={form}
layout="vertical"
onFinish={handleSubmit}
>
<S9TextField
name="name"
label="Customer Name"
rules={[{ required: true, message: 'Name is required' }]}
/>
<S9NumericField
name="creditLimit"
label="Credit Limit"
min={0}
max={1000000}
prefix="$"
/>
<S9Button type="primary" htmlType="submit">
Save Customer
</S9Button>
</S9Form>
);
}

S9Form Props:

  • form?: FormInstance - Form instance from useForm()
  • layout?: 'horizontal' | 'vertical' | 'inline'
  • onFinish?: (values: any) => void - Submit handler
  • onValuesChange?: (changed: any, all: any) => void
  • initialValues?: object - Initial form values

S9TextField​

Text input field with validation.

<S9TextField
name="email"
label="Email Address"
placeholder="Enter email"
rules={[
{ required: true, message: 'Email is required' },
{ type: 'email', message: 'Invalid email format' }
]}
prefix={<MailOutlined />}
maxLength={100}
/>

Props:

  • name: string - Field name
  • label?: string - Field label
  • placeholder?: string - Placeholder text
  • rules?: Rule[] - Validation rules
  • disabled?: boolean - Disable input
  • prefix?: ReactNode - Prefix icon/text
  • suffix?: ReactNode - Suffix icon/text
  • maxLength?: number - Maximum length

S9NumericField​

Number input with formatting options.

<S9NumericField
name="price"
label="Price"
min={0}
max={9999.99}
precision={2}
prefix="$"
formatter={(value) => `$ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
parser={(value) => value.replace(/\$\s?|(,*)/g, '')}
/>

Props:

  • min?: number - Minimum value
  • max?: number - Maximum value
  • step?: number - Step increment
  • precision?: number - Decimal places
  • formatter?: (value: number) => string
  • parser?: (value: string) => number

S9DateField​

Date/time picker with multiple formats.

<S9DateField
name="birthDate"
label="Birth Date"
format="YYYY-MM-DD"
showTime={false}
disabledDate={(date) => date.isAfter(dayjs())}
/>

Props:

  • format?: string - Date format string
  • showTime?: boolean - Include time picker
  • picker?: 'date' | 'week' | 'month' | 'year'
  • disabledDate?: (date: Dayjs) => boolean

S9SingleDropDown / S9MultiDropDown​

Dropdown select components.

<S9SingleDropDown
name="status"
label="Status"
options={[
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' }
]}
showSearch
filterOption={(input, option) =>
option.label.toLowerCase().includes(input.toLowerCase())
}
/>

<S9MultiDropDown
name="tags"
label="Tags"
options={tagOptions}
maxTagCount={3}
mode="multiple"
/>

SingleDropDown Props:

  • options: LabelValue[] - Dropdown options
  • showSearch?: boolean - Enable search
  • filterOption?: (input: string, option: Option) => boolean
  • allowClear?: boolean - Show clear button

MultiDropDown Props:

  • All SingleDropDown props plus:
  • mode?: 'multiple' | 'tags' - Selection mode
  • maxTagCount?: number - Maximum visible tags
  • maxTagTextLength?: number - Maximum tag text length

S9Checkbox​

Checkbox input component.

<S9Checkbox
name="terms"
value="accepted"
rules={[{ required: true, message: 'Please accept terms' }]}
>
I accept the terms and conditions
</S9Checkbox>

S9Radio​

Radio button group.

<S9Radio
name="gender"
label="Gender"
options={[
{ label: 'Male', value: 'M' },
{ label: 'Female', value: 'F' },
{ label: 'Other', value: 'O' }
]}
optionType="button"
/>

S9FileField​

File upload component.

<S9FileField
name="documents"
label="Documents"
multiple
maxCount={5}
accept=".pdf,.doc,.docx"
maxSize={10 * 1024 * 1024} // 10MB
onUpload={async (file) => {
const url = await uploadFile(file);
return { url, name: file.name };
}}
/>

Props:

  • multiple?: boolean - Allow multiple files
  • maxCount?: number - Maximum file count
  • accept?: string - Accepted file types
  • maxSize?: number - Maximum file size in bytes
  • onUpload?: (file: File) => Promise<UploadResult>

S9RichTextEditor​

Rich text editor with formatting tools.

<S9RichTextEditor
name="description"
label="Description"
toolbar={[
'bold', 'italic', 'underline',
'bulletList', 'orderedList',
'link', 'image'
]}
height={300}
/>

S9MonacoEditorField​

Code editor with syntax highlighting.

<S9MonacoEditorField
name="query"
label="SQL Query"
language="sql"
theme="vs-dark"
height={400}
options={{
minimap: { enabled: false },
wordWrap: 'on'
}}
/>

Data Display Components​

S9Table​

Advanced data table with sorting, filtering, and pagination.

import { S9Table } from '@april9au/stack9-ui';

function CustomerTable() {
const columns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
sorter: true,
filters: true
},
{
title: 'Email',
dataIndex: 'email',
key: 'email'
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (status) => (
<S9Tag color={status === 'active' ? 'green' : 'red'}>
{status}
</S9Tag>
)
}
];

return (
<S9Table
columns={columns}
dataSource={customers}
rowKey="id"
pagination={{
pageSize: 20,
showSizeChanger: true
}}
rowSelection={{
type: 'checkbox',
onChange: (keys, rows) => console.log(keys, rows)
}}
/>
);
}

Props:

  • columns: ColumnConfig[] - Column definitions
  • dataSource: any[] - Data array
  • rowKey: string | (record) => string - Unique row key
  • pagination?: PaginationConfig - Pagination settings
  • rowSelection?: RowSelectionConfig - Row selection
  • expandable?: ExpandableConfig - Expandable rows
  • scroll?: { x?: number, y?: number } - Scrolling

S9Tabs​

Tab navigation component.

<S9Tabs
defaultActiveKey="1"
onChange={(key) => console.log('Tab:', key)}
>
<TabPane tab="Details" key="1">
<DetailsPanel />
</TabPane>
<TabPane tab="History" key="2">
<HistoryPanel />
</TabPane>
<TabPane tab="Notes" key="3" disabled>
<NotesPanel />
</TabPane>
</S9Tabs>

S9Fieldset​

Display entity fields in a structured layout.

<S9Fieldset
context={{ id: entityId }}
schema={entitySchema}
formFields={[
{ field: 'name', colSize: 12 },
{ field: 'email', colSize: 12 },
{ field: 'phone', colSize: 8 },
{ field: 'status', colSize: 4 }
]}
mode="view"
/>

Props:

  • context: { id: number } - Entity context
  • schema: EntitySchema - Entity schema
  • formFields: FieldConfig[] - Field configuration
  • mode?: 'view' | 'edit' - Display mode

Descriptions​

Display multiple read-only fields.

<Descriptions bordered column={2}>
<DescriptionItem label="Customer Name">
John Doe
</DescriptionItem>
<DescriptionItem label="Email">
john@example.com
</DescriptionItem>
<DescriptionItem label="Address" span={2}>
123 Main Street, City, State 12345
</DescriptionItem>
</Descriptions>

Action Components​

S9Button​

Enhanced button component with loading states.

<S9Button
type="primary"
icon={<PlusOutlined />}
loading={isLoading}
onClick={handleClick}
>
Add Customer
</S9Button>

Props:

  • type?: 'primary' | 'default' | 'dashed' | 'text' | 'link'
  • danger?: boolean - Red dangerous button
  • loading?: boolean - Loading state
  • icon?: ReactNode - Button icon
  • size?: 'small' | 'middle' | 'large'
  • block?: boolean - Full width button

S9ActionsDropdown​

Dropdown menu for multiple actions.

<S9ActionsDropdown
items={[
{
key: 'edit',
label: 'Edit',
icon: <EditOutlined />,
onClick: () => handleEdit(record)
},
{
key: 'delete',
label: 'Delete',
icon: <DeleteOutlined />,
danger: true,
onClick: () => handleDelete(record)
}
]}
/>

S9EntityActions​

Standard entity action buttons (Edit, Delete, etc.).

<S9EntityActions
entity={customer}
entityKey="customer"
onEdit={() => navigateToEdit(customer.id)}
onDelete={async () => {
await deleteCustomer(customer.id);
refresh();
}}
permissions={{
canEdit: true,
canDelete: userRole === 'admin'
}}
/>

S9EntityWorkflowActions​

Workflow transition buttons for entities.

<S9EntityWorkflowActions
entity={order}
entityKey="order"
currentState="pending"
availableTransitions={[
{ key: 'approve', label: 'Approve', nextState: 'approved' },
{ key: 'reject', label: 'Reject', nextState: 'rejected' }
]}
onTransition={async (transition) => {
await executeTransition(order.id, transition);
}}
/>

Feedback Components​

Alert​

Display alert messages.

<Alert
message="Success"
description="Customer has been created successfully."
type="success"
showIcon
closable
onClose={() => console.log('Alert closed')}
/>

Modal dialog component.

<Modal
title="Confirm Delete"
open={isModalOpen}
onOk={handleOk}
onCancel={handleCancel}
confirmLoading={isDeleting}
>
<p>Are you sure you want to delete this customer?</p>
</Modal>

S9Drawer​

Slide-out panel component.

<S9Drawer
title="Customer Details"
placement="right"
width={720}
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
extra={
<S9Button onClick={handleSave}>Save</S9Button>
}
>
<CustomerForm />
</S9Drawer>

S9Tooltip​

Tooltip component for additional information.

<S9Tooltip title="This field is required">
<InfoCircleOutlined />
</S9Tooltip>

S9Popconfirm​

Confirmation popover.

<S9Popconfirm
title="Delete this item?"
description="This action cannot be undone."
onConfirm={handleDelete}
okText="Yes"
cancelText="No"
>
<S9Button danger>Delete</S9Button>
</S9Popconfirm>

Navigation menu component.

<Menu
mode="inline"
defaultSelectedKeys={['dashboard']}
items={[
{
key: 'dashboard',
icon: <DashboardOutlined />,
label: 'Dashboard'
},
{
key: 'customers',
icon: <UserOutlined />,
label: 'Customers',
children: [
{ key: 'list', label: 'List' },
{ key: 'add', label: 'Add New' }
]
}
]}
onClick={(item) => navigate(item.key)}
/>

Breadcrumb navigation.

<Breadcrumb
items={[
{ title: 'Home', href: '/' },
{ title: 'Customers', href: '/customers' },
{ title: 'John Doe' }
]}
/>

Steps​

Progress steps indicator.

<Steps
current={currentStep}
items={[
{ title: 'Basic Info', description: 'Customer details' },
{ title: 'Address', description: 'Location information' },
{ title: 'Review', description: 'Confirm details' }
]}
/>

Pagination​

Pagination component.

<Pagination
current={currentPage}
total={500}
pageSize={20}
showSizeChanger
showQuickJumper
onChange={(page, pageSize) => {
setCurrentPage(page);
setPageSize(pageSize);
}}
/>

Data Entry Helpers​

S9ScreenQueryDropdown​

Dropdown populated from screen query.

<S9ScreenQueryDropdown
screenName="customer-list"
queryName="active-customers"
valueField="id"
labelField="name"
placeholder="Select customer"
onChange={(value, option) => {
setSelectedCustomer(value);
}}
/>

S9DropdownTypeahead​

Typeahead search dropdown.

<S9DropdownTypeahead
entityName="products"
searchField="name"
valueField="id"
labelField="name"
placeholder="Search products..."
minSearchLength={2}
debounceMs={300}
onSelect={(value, record) => {
setSelectedProduct(record);
}}
/>

S9ColorPickerField​

Color picker component.

<S9ColorPickerField
name="brandColor"
label="Brand Color"
format="hex"
showText
presets={[
{ label: 'Primary', colors: ['#1890ff', '#40a9ff'] },
{ label: 'Success', colors: ['#52c41a', '#73d13d'] }
]}
/>

S9OptionSet​

Option set selector (radio/checkbox group).

<S9OptionSet
name="preferences"
label="Preferences"
options={[
{ label: 'Email Notifications', value: 'email' },
{ label: 'SMS Alerts', value: 'sms' },
{ label: 'Push Notifications', value: 'push' }
]}
type="checkbox"
layout="vertical"
/>

Display Components​

S9Tag​

Tag/label component.

<S9Tag color="green" icon={<CheckCircleOutlined />}>
Active
</S9Tag>

Badge​

Badge for status indicators.

<Badge count={5} overflowCount={99}>
<BellOutlined />
</Badge>

<Badge status="success" text="Online" />

Avatar​

User avatar component.

<Avatar size={64} src={userImage}>
{userName[0]}
</Avatar>

Card​

Content container card.

<Card
title="Customer Details"
extra={<a href="#">More</a>}
actions={[
<EditOutlined key="edit" />,
<DeleteOutlined key="delete" />
]}
>
<p>Card content</p>
</Card>

Collapse​

Collapsible panels.

<Collapse defaultActiveKey={['1']}>
<CollapsePanel header="General Information" key="1">
<GeneralInfo />
</CollapsePanel>
<CollapsePanel header="Contact Details" key="2">
<ContactDetails />
</CollapsePanel>
</Collapse>

Timeline​

Timeline display component.

<Timeline>
<TimelineItem color="green">
Create account - 2024-01-01
</TimelineItem>
<TimelineItem color="blue">
First purchase - 2024-01-15
</TimelineItem>
<TimelineItem dot={<ClockCircleOutlined />}>
Pending approval
</TimelineItem>
</Timeline>

Statistic​

Statistic display component.

<Statistic
title="Total Revenue"
value={112893}
precision={2}
prefix="$"
suffix="USD"
valueStyle={{ color: '#3f8600' }}
/>

Progress​

Progress indicator.

<Progress
percent={75}
status="active"
strokeColor={{
'0%': '#108ee9',
'100%': '#87d068'
}}
/>

Empty​

Empty state component.

<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="No data found"
>
<S9Button type="primary">Create Now</S9Button>
</Empty>

Loading States​

Spin​

Loading spinner.

<Spin size="large" tip="Loading...">
<div style={{ minHeight: 200 }}>
{/* Content that's loading */}
</div>
</Spin>

S9Skeleton​

Skeleton loading placeholder.

<S9Skeleton
active
avatar
paragraph={{ rows: 4 }}
title
/>

Media Components​

Image​

Image display with fallback.

<Image
width={200}
src={imageUrl}
fallback="data:image/png;base64,..."
preview={{
src: fullSizeUrl
}}
/>

S9MediaCard​

Media card for images/videos.

<S9MediaCard
type="image"
src={mediaUrl}
title="Product Image"
description="Main product photo"
actions={[
{ icon: <EyeOutlined />, onClick: handleView },
{ icon: <DeleteOutlined />, onClick: handleDelete }
]}
/>

S9MediaLibrary​

Media library browser.

<S9MediaLibrary
entityKey="product"
entityId={productId}
allowUpload
maxFiles={10}
acceptedTypes={['image/*', 'application/pdf']}
onSelect={(files) => {
setSelectedFiles(files);
}}
/>

Specialized Components​

S9CustomEntityFormPage​

Complete entity form page with validation and submission.

<S9CustomEntityFormPage
entityKey="customer"
entityId={customerId}
mode={customerId ? 'edit' : 'create'}
schema={customerSchema}
onSubmit={async (values) => {
await saveCustomer(values);
navigate('/customers');
}}
onCancel={() => navigate('/customers')}
/>

PrivilegiesMatrix​

Permission matrix component.

<PrivilegiesMatrix
roles={['admin', 'manager', 'user']}
permissions={[
{ key: 'read', label: 'Read' },
{ key: 'write', label: 'Write' },
{ key: 'delete', label: 'Delete' }
]}
values={{
admin: ['read', 'write', 'delete'],
manager: ['read', 'write'],
user: ['read']
}}
onChange={(values) => savePermissions(values)}
/>

SaveFilterButton​

Save and manage filter presets.

<SaveFilterButton
currentFilters={activeFilters}
savedFilters={userSavedFilters}
onSave={(name, filters) => {
saveFilterPreset(name, filters);
}}
onLoad={(filters) => {
applyFilters(filters);
}}
onDelete={(id) => {
deleteFilterPreset(id);
}}
/>

CustomFilterView​

Advanced filter builder.

<CustomFilterView
fields={[
{ key: 'name', label: 'Name', type: 'text' },
{ key: 'status', label: 'Status', type: 'select',
options: statusOptions },
{ key: 'created', label: 'Created', type: 'date' }
]}
onApply={(filters) => {
fetchDataWithFilters(filters);
}}
onReset={() => {
fetchDataWithFilters({});
}}
/>

Complete Page Templates​

Entity List Page​

import { S9Page, S9Table, S9Button } from '@april9au/stack9-ui';

function CustomerListPage() {
return (
<S9Page>
<S9PageHeader
title="Customers"
actions={[
<S9Button type="primary" onClick={handleAdd}>
Add Customer
</S9Button>
]}
/>
<S9PageContent>
<S9Table
columns={columns}
dataSource={customers}
pagination={{ pageSize: 20 }}
/>
</S9PageContent>
</S9Page>
);
}

Entity Detail Page​

function CustomerDetailPage() {
return (
<S9CustomEntityFormPage
entityKey="customer"
entityId={customerId}
mode="view"
tabs={[
{
key: 'details',
label: 'Details',
content: <CustomerDetails />
},
{
key: 'orders',
label: 'Orders',
content: <CustomerOrders />
},
{
key: 'history',
label: 'History',
content: <S9EntityWorkflowHistory
entityKey="customer"
entityId={customerId}
/>
}
]}
/>
);
}

Utility Functions​

The package exports utility functions via the Utils namespace:

import { Utils } from '@april9au/stack9-ui';

// Form utilities
const errors = Utils.validateForm(values, rules);
const formatted = Utils.formatFieldValue(value, fieldType);

// Display utilities
const truncated = Utils.truncateText(text, 100);
const highlighted = Utils.highlightText(text, searchTerm);

// Data utilities
const sorted = Utils.sortData(data, 'name', 'asc');
const filtered = Utils.filterData(data, filters);
const paginated = Utils.paginateData(data, page, pageSize);

Hooks​

useUIComponents​

Access registered UI components.

import { useUIComponents } from '@april9au/stack9-ui';

function DynamicComponent({ componentName }) {
const components = useUIComponents();
const Component = components[componentName];

if (!Component) {
return <div>Component not found</div>;
}

return <Component />;
}

useTheme​

Access and modify theme settings.

import { useTheme } from '@april9au/stack9-ui';

function ThemeToggle() {
const { theme, setTheme } = useTheme();

return (
<Switch
checked={theme === 'dark'}
onChange={(checked) => setTheme(checked ? 'dark' : 'light')}
/>
);
}

useScreenQuery​

Execute named queries from within the current screen context. This hook automatically retrieves the screen key from context and provides SWR-powered caching and revalidation.

import * as ui from '@april9au/stack9-ui';

function CustomerList() {
const { data, error, isLoading, mutate } = ui.useScreenQuery<Customer[]>(
'getcustomerlist',
{
shouldFetch: true,
vars: { page: 0, limit: 20 },
filters: { status: 'Active' },
querySearch: ''
}
);

if (isLoading) return <Spin />;
if (error) return <Alert type="error" message={error.message} />;

return (
<List
dataSource={data}
renderItem={(customer) => (
<List.Item key={customer.id}>
{customer.name}
</List.Item>
)}
/>
);
}

Type Signature:

useScreenQuery<T>(
queryName: string,
options: {
shouldFetch?: boolean;
vars?: Record<string, any>;
filters?: Record<string, any>;
sorting?: Record<string, any>;
querySearch?: string;
}
): SWRResponse<T>

Parameters:

ParameterTypeRequiredDefaultDescription
queryNamestringYes-Name of the query to execute (must be defined in screen's queries)
optionsobjectYes-Query execution options
options.shouldFetchbooleanNotrueWhether to fetch immediately; set to false to defer execution
options.varsRecord<string, any>No{}Variables to pass to the query (replaces {{param}} placeholders)
options.filtersRecord<string, any>No{}Dynamic filters to apply
options.sortingRecord<string, any>No{}Sort configuration
options.querySearchstringNo''Full-text search term

Return Value (SWRResponse):

PropertyTypeDescription
dataT | undefinedQuery results (undefined while loading)
errorError | undefinedError object if query failed
isLoadingbooleanTrue while initial load is in progress
isValidatingbooleanTrue while revalidating data
mutateFunctionManually trigger revalidation or update cache

Common Use Cases:

  1. Simple List Query:
function ProductList() {
const { data: products } = ui.useScreenQuery<Product[]>(
'getproductlist',
{ shouldFetch: true }
);

return <ProductGrid products={products} />;
}
  1. Query with Pagination:
function PaginatedOrders() {
const [page, setPage] = useState(0);

const { data, isLoading } = ui.useScreenQuery<Order[]>(
'getorderlist',
{
vars: { page, limit: 20 }
}
);

return (
<>
<OrderTable data={data} loading={isLoading} />
<Pagination current={page + 1} onChange={(p) => setPage(p - 1)} />
</>
);
}
  1. Query with Search:
function SearchableCustomers() {
const [searchTerm, setSearchTerm] = useState('');

const { data, isLoading } = ui.useScreenQuery<Customer[]>(
'searchcustomers',
{
querySearch: searchTerm,
vars: { limit: 50 }
}
);

return (
<>
<Input.Search onSearch={setSearchTerm} />
<Spin spinning={isLoading}>
<CustomerList customers={data} />
</Spin>
</>
);
}
  1. Conditional Fetching:
function CustomerDetails({ customerId }) {
const [showOrders, setShowOrders] = useState(false);

const { data: orders } = ui.useScreenQuery<Order[]>(
'getcustomerorders',
{
shouldFetch: showOrders && !!customerId,
vars: { customerId }
}
);

return (
<>
<Button onClick={() => setShowOrders(true)}>Load Orders</Button>
{showOrders && <OrderList orders={orders} />}
</>
);
}
  1. Manual Revalidation:
function CustomerListWithRefresh() {
const { data, mutate, isValidating } = ui.useScreenQuery<Customer[]>(
'getcustomerlist',
{ shouldFetch: true }
);

const handleRefresh = () => {
mutate(); // Trigger revalidation
};

return (
<>
<Button onClick={handleRefresh} loading={isValidating}>
Refresh
</Button>
<CustomerList customers={data} />
</>
);
}

Notes:

  • The hook automatically uses the current screen context, so screenKey is not required
  • Must be used within a <ScreenProvider> context
  • Data is cached by SWR based on query name and options
  • Set revalidateOnFocus: false is the default behavior
  • The query name must exist in the screen's queries array or Query Library

useScreenQueryById​

A convenience hook for executing queries that fetch a single record by ID. This is a specialized version of useScreenQuery that automatically structures the query with an id variable and returns the first item if the result is an array.

import * as ui from '@april9au/stack9-ui';

function CustomerDetail({ customerId }) {
const { data: customer, error, isLoading } = ui.useScreenQueryById<Customer>(
'getcustomer',
customerId,
{ shouldFetch: true }
);

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 (
<Descriptions title="Customer Details">
<Descriptions.Item label="Name">{customer.name}</Descriptions.Item>
<Descriptions.Item label="Email">{customer.email}</Descriptions.Item>
<Descriptions.Item label="Status">{customer.status}</Descriptions.Item>
</Descriptions>
);
}

Type Signature:

useScreenQueryById<T>(
queryName: string,
id: number | undefined,
opts?: {
shouldFetch?: boolean;
}
): SWRResponse<T>

Parameters:

ParameterTypeRequiredDefaultDescription
queryNamestringYes-Name of the query to execute
idnumber | undefinedYes-The ID value to pass to the query; query won't execute if undefined
optsobjectNo{}Additional options
opts.shouldFetchbooleanNotrueWhether to fetch immediately

Return Value:

Same as useScreenQuery, but data is automatically unwrapped to a single object if the query returns an array.

Common Use Cases:

  1. Detail View:
function OrderDetail({ orderId }) {
const { data: order, isLoading } = ui.useScreenQueryById<Order>(
'getorderdetails',
orderId
);

if (isLoading) return <Spin />;

return (
<Card title={`Order #${order?.order_number}`}>
<OrderInfo order={order} />
</Card>
);
}
  1. Conditional Loading Based on ID:
function EditCustomerForm({ customerId }) {
// Only fetch if customerId is provided (edit mode)
const { data: customer } = ui.useScreenQueryById<Customer>(
'getcustomer',
customerId // undefined in create mode, number in edit mode
);

return (
<Form initialValues={customer}>
{/* Form fields */}
</Form>
);
}
  1. With Route Parameters:
import { useParams } from 'react-router-dom';

function ProductDetailPage() {
const { id } = useParams<{ id: string }>();

const { data: product, error } = ui.useScreenQueryById<Product>(
'getproduct',
id ? parseInt(id) : undefined
);

if (!id) return <Navigate to="/products" />;
if (error) return <ErrorPage error={error} />;

return <ProductDetails product={product} />;
}
  1. Related Data Loading:
function CustomerWithOrders({ customerId }) {
const { data: customer } = ui.useScreenQueryById<Customer>(
'getcustomer',
customerId
);

const { data: recentOrders } = ui.useScreenQuery<Order[]>(
'getcustomerrecentorders',
{
shouldFetch: !!customerId,
vars: { customerId, limit: 5 }
}
);

return (
<>
<CustomerCard customer={customer} />
<RecentOrders orders={recentOrders} />
</>
);
}
  1. Optimistic Updates:
function CustomerActions({ customerId }) {
const { data: customer, mutate } = ui.useScreenQueryById<Customer>(
'getcustomer',
customerId
);

const handleStatusChange = async (newStatus: string) => {
// Optimistically update the UI
mutate(
{ ...customer, status: newStatus },
false // Don't revalidate immediately
);

try {
await updateCustomerStatus(customerId, newStatus);
mutate(); // Revalidate after success
} catch (error) {
mutate(); // Revert on error
}
};

return <StatusSelector value={customer?.status} onChange={handleStatusChange} />;
}

Comparison with useScreenQuery:

FeatureuseScreenQueryuseScreenQueryById
Use caseLists, complex queriesSingle record by ID
ID parameterPass in vars: { id }Dedicated id parameter
Return valueRaw query resultUnwrapped single object
SyntaxMore flexibleMore concise for ID queries

When to Use Each:

Use useScreenQueryById:

  • Fetching a single record by ID
  • Detail views, edit forms
  • When the query is defined to return a single record

Use useScreenQuery:

  • List views with pagination
  • Complex queries with multiple parameters
  • Queries with filters and search
  • When you need full control over query options

Notes:

  • If id is undefined, the query will not execute
  • If the query returns an array, only the first item is returned as data
  • The query should be designed to return a single record or an array with one item
  • Must be used within a <ScreenProvider> context
  • Internally calls runNamedQuery() with { vars: { id } }

Theming and Customization​

Custom Theme​

import { ConfigProvider } from 'antd';

const customTheme = {
token: {
colorPrimary: '#00b96b',
borderRadius: 8,
fontFamily: 'Inter, sans-serif'
}
};

function App() {
return (
<ConfigProvider theme={customTheme}>
<UIProvider>
{/* Your app */}
</UIProvider>
</ConfigProvider>
);
}

Component Registration​

Register custom components for use in dynamic screens:

import { UIProvider } from '@april9au/stack9-ui';
import { CustomWidget } from './components';

const components = [
// Stack9 components
S9Button,
S9Table,
// Your custom components
CustomWidget
];

function App() {
return (
<UIProvider components={components}>
{/* Components now available in screens */}
</UIProvider>
);
}

Best Practices​

1. Form Handling​

Always use form instances for complex forms:

function ComplexForm() {
const [form] = S9Form.useForm();

// Programmatically set values
useEffect(() => {
form.setFieldsValue({
name: defaultName
});
}, [defaultName]);

// Validate before submit
const handleSubmit = async () => {
try {
const values = await form.validateFields();
await saveData(values);
} catch (error) {
message.error('Please fix form errors');
}
};
}

2. Table Performance​

For large datasets, use server-side pagination:

function LargeTable() {
const [pagination, setPagination] = useState({
current: 1,
pageSize: 20,
total: 0
});

const fetchData = async (params) => {
const result = await api.getData({
page: params.current,
limit: params.pageSize
});

setData(result.items);
setPagination({
...params,
total: result.total
});
};

return (
<S9Table
columns={columns}
dataSource={data}
pagination={pagination}
onChange={(pag) => fetchData(pag)}
/>
);
}

3. Component Composition​

Build reusable compound components:

function CustomerCard({ customer }) {
return (
<Card>
<Card.Meta
avatar={<Avatar>{customer.name[0]}</Avatar>}
title={customer.name}
description={customer.email}
/>
<Descriptions>
<DescriptionItem label="Phone">
{customer.phone}
</DescriptionItem>
<DescriptionItem label="Status">
<S9Tag color={customer.active ? 'green' : 'red'}>
{customer.active ? 'Active' : 'Inactive'}
</S9Tag>
</DescriptionItem>
</Descriptions>
</Card>
);
}

4. Error Handling​

Implement comprehensive error boundaries:

function SafeComponent({ children }) {
return (
<ErrorBoundary
fallback={
<S9Result
status="500"
title="Something went wrong"
subTitle="Please refresh the page or contact support"
extra={
<S9Button type="primary" onClick={() => window.location.reload()}>
Refresh Page
</S9Button>
}
/>
}
>
{children}
</ErrorBoundary>
);
}

Migration Guide​

From Ant Design to Stack9 UI​

Replace Ant Design imports with Stack9 UI equivalents:

// Before
import { Button, Table, Form } from 'antd';

// After
import { S9Button, S9Table, S9Form } from '@april9au/stack9-ui';

Component usage remains similar with enhanced props:

// Before
<Button type="primary" loading={loading}>
Submit
</Button>

// After
<S9Button type="primary" loading={loading}>
Submit
</S9Button>

Troubleshooting​

Common Issues​

Issue: Components not rendering

  • Solution: Ensure UIProvider wraps your app and components are registered

Issue: Form validation not working

  • Solution: Check that form fields have name prop and are wrapped in S9Form

Issue: Table sorting/filtering not working

  • Solution: Implement server-side handlers for onChange prop

Issue: Theme not applying

  • Solution: Wrap with ConfigProvider and ensure it's above UIProvider

Support​

For issues, feature requests, or questions: