Ant Design Guide
Use when building or reviewing @faasjs/ant-design feature UI, CRUD surfaces, app feedback, modals, and drawers.
Applicable Scenarios
- Creating feature UI under
features/ - Building list, detail, create, update, or delete flows
- Deciding how to split feature frontend files
- Choosing between
faas,useFaas,useFaasStream,faasData, and custom hooks for requests - Implementing message prompts, notifications, confirmation modals, or drawer workflows
Default Workflow
- Follow File Conventions and place features under
features/<feature-name>/. - Use
Apponce near the frontend root. - Keep feature entries mostly compositional; move concrete UI to
components/only when it earns a boundary. - Put feature-local request files under
api/and keep action paths aligned with file paths. - Model business fields as shared
itemsmetadata reused byForm,Description, andTable. - Start CRUD with
Table,Description, andForm; useuseApp()formessage,notification,setModalProps, andsetDrawerProps. - Import re-exported request helpers from
@faasjs/ant-design, includingfaas,useFaas,useFaasStream,FaasReactClient,FaasDataWrapper, andwithFaasData. - Keep component inputs readable as
props.xxx; do not destructure props in component parameters.
Recommended Layout
src/features/users/
index.tsx
components/
UserDescription.tsx
UserForm.tsx
UserTable.tsx
hooks/
useUserItems.ts
api/
create.api.ts
detail.api.ts
list.api.ts
remove.api.ts
update.api.ts
This keeps:
- Feature entry UI in
index.tsx - Feature UI in
components/ - Feature-specific reusable logic in
hooks/ - Backend handlers in
api/
Actions map directly to:
/features/users/api/list/features/users/api/detail/features/users/api/create/features/users/api/update/features/users/api/remove
Core Patterns
Feature entries
Feature entry files should focus on composing existing components and triggering shared app interactions through useApp:
import { Button, Space } from 'antd'
import { useApp } from '@faasjs/ant-design'
import { UserForm } from './components/UserForm'
import { UserTable } from './components/UserTable'
export default function UsersPage() {
const { setDrawerProps } = useApp()
return (
<Space direction="vertical" style={{ width: '100%' }}>
<Button
type="primary"
onClick={() =>
setDrawerProps({
open: true,
title: 'Create User',
width: 720,
children: <UserForm />,
})
}
>
Create User
</Button>
<UserTable />
</Space>
)
}
List, detail, edit, delete
For most CRUD feature UI:
- Use
Tablefor the list,Descriptionfor detail, andFormfor create/edit. - Open detail and edit panels in drawers to keep list context visible.
- Use modal confirmations for destructive actions.
- Reuse
itemsacross list, detail, and form.
import { Button, Space } from 'antd'
import { faas, Table, useApp } from '@faasjs/ant-design'
import { useUserItems } from '../hooks/useUserItems'
import { UserDescription } from './UserDescription'
import { UserForm } from './UserForm'
export function UserTable() {
const { message, setDrawerProps, setModalProps } = useApp()
const items = useUserItems()
return (
<Table
rowKey="id"
items={[
...items,
{
id: 'actions',
title: 'Actions',
tableRender: (_, row) => (
<Space>
<Button
size="small"
onClick={() =>
setDrawerProps({
open: true,
title: `User #${row.id}`,
width: 720,
children: <UserDescription id={row.id} />,
})
}
>
Detail
</Button>
<Button
size="small"
onClick={() =>
setDrawerProps({
open: true,
title: `Edit User #${row.id}`,
width: 720,
children: <UserForm id={row.id} initialValues={row} />,
})
}
>
Edit
</Button>
<Button
danger
size="small"
onClick={() =>
setModalProps({
open: true,
title: 'Delete User',
children: 'This action cannot be undone.',
onOk: async () => {
await faas('features/users/api/remove', { id: row.id })
message.success('User deleted')
setModalProps({ open: false })
},
})
}
>
Delete
</Button>
</Space>
),
},
]}
faasData={{
action: 'features/users/api/list',
}}
pagination={{
pageSize: 20,
}}
onRow={(record) => ({
onDoubleClick: () =>
setDrawerProps({
open: true,
title: `User #${record.id}`,
width: 720,
children: <UserDescription id={record.id} />,
}),
})}
/>
)
}
Detail view
import { Description } from '@faasjs/ant-design'
import { useUserItems } from '../hooks/useUserItems'
export function UserDescription(props: { id: number }) {
const items = useUserItems()
return (
<Description
column={1}
items={items}
faasData={{
action: 'features/users/api/detail',
params: {
id: props.id,
},
}}
/>
)
}
Create and update forms
- Prefer the
Formfaasprop when the submission flow is a single direct action call. - Load edit data outside the form and pass it via
initialValues. - Let
useApphandle success and failure feedback.
import { Form, useApp } from '@faasjs/ant-design'
import { useUserItems } from '../hooks/useUserItems'
export function UserForm(props: { id?: number; initialValues?: Record<string, any> }) {
const { message, notification, setDrawerProps } = useApp()
const items = useUserItems()
return (
<Form
initialValues={props.initialValues}
items={items}
faas={{
action: props.id ? 'features/users/api/update' : 'features/users/api/create',
params: (values) => ({
...values,
...(props.id ? { id: props.id } : {}),
}),
onSuccess: () => {
message.success(props.id ? 'User updated' : 'User created')
setDrawerProps({ open: false })
},
onError: (error) => {
notification.error({
message: props.id ? 'Update failed' : 'Create failed',
description: error?.message || 'Unknown error',
})
},
}}
/>
)
}
Delete and dangerous actions
- Prefer
useApp().setModalProps(...)over scattered local modal instances. - Use
messagefor short success feedback andnotificationfor more complete failure feedback.
import { Button } from 'antd'
import { faas, useApp } from '@faasjs/ant-design'
export function RemoveButton(props: { id: number }) {
const { message, notification, setModalProps } = useApp()
return (
<Button
danger
onClick={() =>
setModalProps({
open: true,
title: 'Delete User',
children: 'Please confirm the deletion.',
onOk: async () => {
try {
await faas('features/users/api/remove', { id: props.id })
message.success('User deleted')
setModalProps({ open: false })
} catch (error: any) {
notification.error({
message: 'Delete failed',
description: error?.message || 'Unknown error',
})
}
},
})
}
>
Delete
</Button>
)
}
Preferred Components
Title
Use for page or section headings. Prefer over hand-written heading markup or setting document.title manually in app surfaces.
import { Title } from '@faasjs/ant-design'
export default function UsersPage() {
return <Title title={['Users', 'List']} h1 />
}
Tabs
Use for tabbed business views that should share the FaasJS/Ant Design look. Prefer the id-driven items shape over raw Ant Design Tabs.
import { Tabs } from '@faasjs/ant-design'
export function UserTabs() {
return (
<Tabs
items={[
{ id: 'profile', children: <div>Profile</div> },
{ id: 'logs', children: <div>Logs</div> },
]}
/>
)
}
Blank
Use for empty field display instead of scattering '-', 'N/A', or empty fragments through templates.
import { Blank } from '@faasjs/ant-design'
export function UserEmail(props: { email?: string | null }) {
return <Blank value={props.email} />
}
Loading
Use for explicit loading surfaces outside component-owned faasData lifecycles. Also serves as the default fallback for FaasDataWrapper.
import { Loading } from '@faasjs/ant-design'
export function UserPanel(props: { loading: boolean }) {
return (
<Loading loading={props.loading}>
<div>Loaded content</div>
</Loading>
)
}
ConfigProvider
Use for subtree-level overrides of FaasJS Ant Design copy, theme defaults, or client behavior. Prefer over scattering configuration into leaf components.
import { Blank, ConfigProvider } from '@faasjs/ant-design'
export function EmptyState() {
return (
<ConfigProvider theme={{ common: { blank: 'No data' } }}>
<Blank />
</ConfigProvider>
)
}
ErrorBoundary
Use for unstable or isolated areas to prevent render errors from taking down the entire page.
import { ErrorBoundary } from '@faasjs/ant-design'
export function Page() {
return (
<ErrorBoundary>
<DangerousWidget />
</ErrorBoundary>
)
}
FaasDataWrapper / withFaasData
Use when wrapper composition or fixed integration boundaries help, for example summary cards, dashboard widgets, or small async components.
import { FaasDataWrapper } from '@faasjs/ant-design'
export function UserSummary(props: { id: number }) {
return (
<FaasDataWrapper action="features/users/api/detail" params={{ id: props.id }}>
{(result) => <div>{result.data?.name}</div>}
</FaasDataWrapper>
)
}
useThemeToken
Use for custom layout tokens instead of hardcoding common spacing, radius, or colors. Essential when writing custom div-based blocks that must stay visually consistent with the rest of the app.
import { useThemeToken } from '@faasjs/ant-design'
export function Section() {
const { colorPrimary, borderRadius } = useThemeToken()
return <div style={{ border: `1px solid ${colorPrimary}`, borderRadius }} />
}
useModal / useDrawer
Use only for intentionally isolated local instances outside the shared App shell. In regular feature UI, prefer useApp().setModalProps(...) and useApp().setDrawerProps(...).
import { Button } from 'antd'
import { useDrawer } from '@faasjs/ant-design'
export function LocalPreview() {
const { drawer, setDrawerProps } = useDrawer()
return (
<>
<Button
onClick={() => setDrawerProps({ open: true, title: 'Preview', children: <div>Body</div> })}
>
Open
</Button>
{drawer}
</>
)
}
Rules
-
Follow the
features/,components/,hooks/,api/structure and action-path mapping for feature-local APIs. Feature UI lives underfeatures/<feature>/, entry files useindex.tsx, components go incomponents/, hooks inhooks/, request handlers inapi/. -
Use
Apponce as the application shell; do not scatter independent app shells through features.Appowns sharedmessage,notification,modal, anddrawerbehavior. Only drop down toConfigProviderwhen a smaller boundary is intentional. -
Treat
itemsas the source of truth for business fields acrossForm,Description, andTable. Start withid,type,title,options, and nestedobjectdefinitions. Reuse the same metadata across surfaces unless domain semantics truly diverge. -
Prefer FaasJS wrappers over raw Ant Design primitives when the wrapper fits common CRUD, loading, empty, or feedback surfaces. Start from
Table,Description,Form,Title,Tabs,Blank,Loading,ErrorBoundary, andFaasDataWrapperbefore reaching for raw primitives. -
If custom layout is necessary, read visual values from
useThemeToken()instead of hardcoding tokens.Prefer:
import { useThemeToken } from '@faasjs/ant-design' export function SummaryCard(props: { children: React.ReactNode }) { const { colorBorder, borderRadiusLG, padding } = useThemeToken() return ( <div style={{ padding, border: `1px solid ${colorBorder}`, borderRadius: borderRadiusLG }}> {props.children} </div> ) }Avoid:
export function SummaryCard(props: { children: React.ReactNode }) { return ( <div style={{ padding: 12, border: '1px solid #d9d9d9', borderRadius: 8 }}> {props.children} </div> ) } -
Use the
Formfaasprop,TablefaasDataprop,DescriptionfaasDataprop, andfaasfor straightforward request lifecycles before custom loading/effect plumbing. Lean on built-in request props rather than wiring manual loading state and effect-based glue. -
Use
useApp()for shared feedback and overlays. Usemessagefor lightweight success/warning feedback,notificationfor persistent feedback with title and description,setModalPropsfor confirmations, andsetDrawerPropsfor create/edit/detail panels that should preserve feature context. Prefer drawers for in-context create/edit/detail and modals for confirmations. Use localuseModaloruseDraweronly when creating isolated instances outside the shared app shell. -
Import request helpers that
@faasjs/ant-designre-exports from@faasjs/ant-design, not@faasjs/react, includingfaas,useFaas,useFaasStream,FaasReactClient,FaasDataWrapper, andwithFaasData. This keeps failed request feedback aligned with the sharedAppconfiguration. -
Promote repeated custom field behavior into
extendTypes; keep one-off customization on the item itself.import { Form, type ExtendFormItemProps, type FormProps } from '@faasjs/ant-design' import { Input } from 'antd' interface UserFormItem extends ExtendFormItemProps { type: 'password' } function UserForm(props: FormProps<any, UserFormItem>) { return ( <Form {...props} extendTypes={{ password: { children: <Input.Password />, }, }} /> ) } -
Use
formRender,descriptionRender, ortableRenderonly when a field truly differs by surface; do not fork fake field ids.childrenandrenderare general overrides, whileformRender,descriptionRender, andtableRenderare surface-specific. Maintain shared metadata until presentation genuinely diverges.
Review Checklist
- feature layout and action paths follow the
features/,components/,hooks/,api/structure - API files align with action-path mapping conventions
- feature entry composes feature components instead of containing all logic inline
- shared
itemsmetadata drivesForm,Description, andTable - wrappers and
faas/faasDatacover straightforward request flows - re-exported request helpers are imported from
@faasjs/ant-design, not@faasjs/react - CRUD feature UI primarily uses
Table,Description, andForm - FaasJS wrapper components are preferred over raw Ant Design primitives where they fit
- custom
div-based UI is not written unless existing components are insufficient - custom layout uses
useThemeTokeninstead of hardcoded values - user feedback is centralized through
useAppinstead of scattered local message/modal instances - create/edit overlays use
setDrawerPropswhen feature context should be preserved - destructive confirmations use
setModalProps - repeated custom field behavior is promoted to
extendTypes - surface-specific overrides are used only when rendering truly differs