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: