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 componentsextensions: 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 definitionsonRouteChange: (route: string) => void- Route change callbacktheme: 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 titlesubtitle?: string- Page subtitlebreadcrumb?: BreadcrumbItem[]- Breadcrumb navigationactions?: ReactNode[]- Action buttonsextra?: 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 columnsalign?: 'top' | 'middle' | 'bottom'- Vertical alignmentjustify?: 'start' | 'center' | 'end' | 'space-around' | 'space-between'
S9Col Props:
span?: number- Column width (1-24)offset?: number- Column offsetxs, 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 handleronValuesChange?: (changed: any, all: any) => voidinitialValues?: 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 namelabel?: string- Field labelplaceholder?: string- Placeholder textrules?: Rule[]- Validation rulesdisabled?: boolean- Disable inputprefix?: ReactNode- Prefix icon/textsuffix?: ReactNode- Suffix icon/textmaxLength?: 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 valuemax?: number- Maximum valuestep?: number- Step incrementprecision?: number- Decimal placesformatter?: (value: number) => stringparser?: (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 stringshowTime?: boolean- Include time pickerpicker?: '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 optionsshowSearch?: boolean- Enable searchfilterOption?: (input: string, option: Option) => booleanallowClear?: boolean- Show clear button
MultiDropDown Props:
- All SingleDropDown props plus:
mode?: 'multiple' | 'tags'- Selection modemaxTagCount?: number- Maximum visible tagsmaxTagTextLength?: 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 filesmaxCount?: number- Maximum file countaccept?: string- Accepted file typesmaxSize?: number- Maximum file size in bytesonUpload?: (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 definitionsdataSource: any[]- Data arrayrowKey: string | (record) => string- Unique row keypagination?: PaginationConfig- Pagination settingsrowSelection?: RowSelectionConfig- Row selectionexpandable?: ExpandableConfig- Expandable rowsscroll?: { 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 contextschema: EntitySchema- Entity schemaformFields: FieldConfig[]- Field configurationmode?: '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 buttonloading?: boolean- Loading stateicon?: ReactNode- Button iconsize?: '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
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 Components
Menu
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
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:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
queryName | string | Yes | - | Name of the query to execute (must be defined in screen's queries) |
options | object | Yes | - | Query execution options |
options.shouldFetch | boolean | No | true | Whether to fetch immediately; set to false to defer execution |
options.vars | Record<string, any> | No | {} | Variables to pass to the query (replaces {{param}} placeholders) |
options.filters | Record<string, any> | No | {} | Dynamic filters to apply |
options.sorting | Record<string, any> | No | {} | Sort configuration |
options.querySearch | string | No | '' | Full-text search term |
Return Value (SWRResponse):
| Property | Type | Description |
|---|---|---|
data | T | undefined | Query results (undefined while loading) |
error | Error | undefined | Error object if query failed |
isLoading | boolean | True while initial load is in progress |
isValidating | boolean | True while revalidating data |
mutate | Function | Manually trigger revalidation or update cache |
Common Use Cases:
- Simple List Query:
function ProductList() {
const { data: products } = ui.useScreenQuery<Product[]>(
'getproductlist',
{ shouldFetch: true }
);
return <ProductGrid products={products} />;
}
- 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)} />
</>
);
}
- 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>
</>
);
}
- 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} />}
</>
);
}
- 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
screenKeyis not required - Must be used within a
<ScreenProvider>context - Data is cached by SWR based on query name and options
- Set
revalidateOnFocus: falseis the default behavior - The query name must exist in the screen's
queriesarray 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:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
queryName | string | Yes | - | Name of the query to execute |
id | number | undefined | Yes | - | The ID value to pass to the query; query won't execute if undefined |
opts | object | No | {} | Additional options |
opts.shouldFetch | boolean | No | true | Whether 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:
- 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>
);
}
- 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>
);
}
- 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} />;
}
- 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} />
</>
);
}
- 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:
| Feature | useScreenQuery | useScreenQueryById |
|---|---|---|
| Use case | Lists, complex queries | Single record by ID |
| ID parameter | Pass in vars: { id } | Dedicated id parameter |
| Return value | Raw query result | Unwrapped single object |
| Syntax | More flexible | More 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
idisundefined, 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
nameprop and are wrapped in S9Form
Issue: Table sorting/filtering not working
- Solution: Implement server-side handlers for
onChangeprop
Issue: Theme not applying
- Solution: Wrap with ConfigProvider and ensure it's above UIProvider
Support
For issues, feature requests, or questions:
- GitHub Issues: stack9-monorepo/issues
- Documentation: stack9.docs
- Community: Stack9 Discord