Skip to main content

Built-in Components

DEVELOPER Beginner

Use pre-built React components from EZ-Console framework.

Overview

EZ-Console provides a set of built-in React components that simplify common development tasks. These components include the main application shell, permission guards, layouts, and more. This guide covers how to use these components in your applications.

Built-in Components

EZApp

The main application component that provides the entire app structure, including layout, navigation, authentication flow, and route protection.

import { EZApp } from 'ez-console';

export default function App() {
return <EZApp basePath='/' />;
}

Props

  • basePath (string): Base path for the application (e.g., '/' or '/app')
  • extraPrivateRoutes (Route[]): Additional private routes to add to the app
  • extraPublicRoutes (Route[]): Additional public routes (e.g., login, register)

Example: Adding Custom Routes

import { EZApp, withSuspense } from 'ez-console';
import { lazy } from 'react';

const ProductPage = lazy(() => import('@/pages/ProductPage'));
const OrderPage = lazy(() => import('@/pages/OrderPage'));

export default function App() {
return (
<EZApp
basePath='/'
extraPrivateRoutes={[
{
path: '/products',
element: withSuspense(ProductPage),
name: 'products',
},
{
path: '/orders',
element: withSuspense(OrderPage),
name: 'orders',
},
]}
/>
);
}

Built-in Features

The EZApp component provides:

  • ✅ Layout (header, sidebar, content area)
  • ✅ Navigation menu with automatic route detection
  • ✅ Authentication flow and route protection
  • ✅ User profile dropdown menu
  • ✅ Language switcher
  • ✅ Global state providers (Auth, Theme, etc.)

PermissionGuard

Conditionally renders content based on user permissions. This component integrates with EZ-Console's RBAC system.

import { PermissionGuard } from 'ez-console';

export const ProductActions: React.FC = () => {
return (
<Space>
<PermissionGuard permission="product:create">
<Button type="primary">Create Product</Button>
</PermissionGuard>

<PermissionGuard permission="product:update">
<Button>Edit Product</Button>
</PermissionGuard>

<PermissionGuard permission="product:delete">
<Button danger>Delete Product</Button>
</PermissionGuard>
</Space>
);
};

Props

  • permission (string): Permission string in format Module:Resource:Action (e.g., "product:create")
  • children (ReactNode): Content to render if permission is granted

Layout

Pre-built layout component with sidebar and header. This is automatically included in EZApp but can be used standalone if needed.

import { Layout } from 'ez-console';

// Already included in EZApp
// Customize through theme configuration or props

Loading

Simple loading spinner component for indicating loading states.

import { Loading } from 'ez-console';

export const MyComponent: React.FC = () => {
const { loading } = useRequest(() => apiGet('/data'));

if (loading) {
return <Loading />;
}

return <div>Content</div>;
};

Creating Reusable Components

While EZ-Console provides core components, you'll often want to create your own reusable components. Here are some common patterns:

DataTable Component

A reusable table component with action buttons:

// src/components/DataTable.tsx
import { Table, Button, Space } from 'antd';
import { TableProps } from 'antd/es/table';
import { EditOutlined, DeleteOutlined } from '@ant-design/icons';

interface DataTableProps<T> extends TableProps<T> {
onEdit?: (record: T) => void;
onDelete?: (record: T) => void;
canEdit?: boolean;
canDelete?: boolean;
}

export function DataTable<T extends { id: string }>({
onEdit,
onDelete,
canEdit = false,
canDelete = false,
columns,
...props
}: DataTableProps<T>) {
const actionColumn = {
title: 'Actions',
key: 'actions',
fixed: 'right' as const,
width: 150,
render: (_: any, record: T) => (
<Space>
{canEdit && onEdit && (
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => onEdit(record)}
>
Edit
</Button>
)}
{canDelete && onDelete && (
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
onClick={() => onDelete(record)}
>
Delete
</Button>
)}
</Space>
),
};

const finalColumns = [...(columns || []), actionColumn];

return <Table columns={finalColumns} {...props} />;
}

Usage

import { DataTable } from '@/components/DataTable';

export const ProductList: React.FC = () => {
const { data, loading } = useRequest(() => apiGet('/products'));

const handleEdit = (product: Product) => {
// Navigate to edit page
};

const handleDelete = (product: Product) => {
// Show delete confirmation
};

return (
<DataTable
dataSource={data}
loading={loading}
columns={[
{ title: 'Name', dataIndex: 'name', key: 'name' },
{ title: 'Price', dataIndex: 'price', key: 'price' },
]}
onEdit={handleEdit}
onDelete={handleDelete}
canEdit={true}
canDelete={true}
/>
);
};

FormModal Component

A reusable modal component with form handling:

// src/components/FormModal.tsx
import { Modal, Form, FormInstance } from 'antd';
import { ReactNode } from 'react';

interface FormModalProps {
title: string;
open: boolean;
loading?: boolean;
form: FormInstance;
onOk: () => void;
onCancel: () => void;
children: ReactNode;
width?: number;
}

export const FormModal: React.FC<FormModalProps> = ({
title,
open,
loading,
form,
onOk,
onCancel,
children,
width = 520,
}) => {
return (
<Modal
title={title}
open={open}
onOk={onOk}
onCancel={onCancel}
confirmLoading={loading}
width={width}
destroyOnClose
>
<Form form={form} layout="vertical">
{children}
</Form>
</Modal>
);
};

Usage

import { FormModal } from '@/components/FormModal';

export const ProductForm: React.FC = () => {
const [form] = Form.useForm();
const [open, setOpen] = useState(false);

const { run: createProduct, loading } = useRequest(
(values) => apiPost('/products', values),
{ manual: true, onSuccess: () => setOpen(false) }
);

const handleSubmit = () => {
form.validateFields().then((values) => {
createProduct(values);
});
};

return (
<FormModal
title="Create Product"
open={open}
loading={loading}
form={form}
onOk={handleSubmit}
onCancel={() => setOpen(false)}
>
<Form.Item name="name" label="Name" rules={[{ required: true }]}>
<Input />
</Form.Item>
</FormModal>
);
};

SearchForm Component

A reusable search form component:

// src/components/SearchForm.tsx
import { Form, Input, Button, Row, Col, Space } from 'antd';
import { FormInstance } from 'antd/es/form';
import { useTranslation } from 'ez-console';

interface SearchFormProps {
form: FormInstance;
onSearch: (values: any) => void;
onReset: () => void;
children?: React.ReactNode;
}

export const SearchForm: React.FC<SearchFormProps> = ({
form,
onSearch,
onReset,
children,
}) => {
const { t } = useTranslation();

return (
<Form form={form} layout="vertical" onFinish={onSearch}>
<Row gutter={16}>
{children}
</Row>
<Row style={{ marginTop: 16 }}>
<Col>
<Space>
<Button type="primary" htmlType="submit">
{t('common.search')}
</Button>
<Button onClick={onReset}>
{t('common.reset')}
</Button>
</Space>
</Col>
</Row>
</Form>
);
};

Usage

import { SearchForm } from '@/components/SearchForm';

export const ProductSearch: React.FC = () => {
const [form] = Form.useForm();

const handleSearch = (values: any) => {
// Perform search
};

const handleReset = () => {
form.resetFields();
handleSearch({});
};

return (
<SearchForm form={form} onSearch={handleSearch} onReset={handleReset}>
<Col span={8}>
<Form.Item name="name" label="Name">
<Input placeholder="Search by name" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="category" label="Category">
<Select placeholder="Select category">
<Option value="electronics">Electronics</Option>
<Option value="clothing">Clothing</Option>
</Select>
</Form.Item>
</Col>
</SearchForm>
);
};

ConfirmDelete Hook

A custom hook for delete confirmation dialogs:

// src/components/ConfirmDelete.tsx
import { Modal } from 'antd';
import { useTranslation } from 'ez-console';
import { ExclamationCircleOutlined } from '@ant-design/icons';

export const useConfirmDelete = () => {
const { t } = useTranslation();

return (onConfirm: () => void, itemName?: string) => {
Modal.confirm({
title: t('common.confirmDelete'),
icon: <ExclamationCircleOutlined />,
content: itemName
? t('common.confirmDeleteItem', { item: itemName })
: t('common.confirmDeleteMessage'),
okText: t('common.confirm'),
okType: 'danger',
cancelText: t('common.cancel'),
onOk: onConfirm,
});
};
};

Usage

import { useConfirmDelete } from '@/components/ConfirmDelete';

export const ProductList: React.FC = () => {
const confirmDelete = useConfirmDelete();

const { run: deleteProduct } = useRequest(
(id: string) => apiDelete(`/products/${id}`),
{ manual: true }
);

const handleDelete = (product: Product) => {
confirmDelete(() => {
deleteProduct(product.id);
}, product.name);
};

return (
<Button danger onClick={() => handleDelete(product)}>
Delete
</Button>
);
};

Component Patterns

List-Detail Pattern

A common pattern where clicking an item shows its details:

// src/pages/products/index.tsx
import { useState } from 'react';
import ProductList from './ProductList';
import ProductDetail from './ProductDetail';

export const ProductsPage: React.FC = () => {
const [selectedId, setSelectedId] = useState<string | null>(null);

if (selectedId) {
return (
<ProductDetail
id={selectedId}
onBack={() => setSelectedId(null)}
/>
);
}

return (
<ProductList
onSelect={(id) => setSelectedId(id)}
/>
);
};

Master-Detail Pattern

Using tabs to show different aspects of a single entity:

// src/pages/orders/OrderDetail.tsx
import { Tabs } from 'antd';
import OrderInfo from './OrderInfo';
import OrderItems from './OrderItems';
import OrderHistory from './OrderHistory';

export const OrderDetail: React.FC<{ id: string }> = ({ id }) => {
return (
<Tabs
items={[
{
key: 'info',
label: 'Order Information',
children: <OrderInfo id={id} />,
},
{
key: 'items',
label: 'Items',
children: <OrderItems id={id} />,
},
{
key: 'history',
label: 'History',
children: <OrderHistory id={id} />,
},
]}
/>
);
};

Form with Steps

Multi-step forms for complex data entry:

// src/pages/products/ProductWizard.tsx
import { Steps, Button, Card } from 'antd';
import { useState } from 'react';

export const ProductWizard: React.FC = () => {
const [current, setCurrent] = useState(0);
const [formData, setFormData] = useState({});

const steps = [
{
title: 'Basic Info',
content: <BasicInfoForm data={formData} onChange={setFormData} />,
},
{
title: 'Pricing',
content: <PricingForm data={formData} onChange={setFormData} />,
},
{
title: 'Images',
content: <ImagesForm data={formData} onChange={setFormData} />,
},
];

return (
<Card>
<Steps current={current} items={steps} style={{ marginBottom: 24 }} />
<div>{steps[current].content}</div>
<div style={{ marginTop: 24, textAlign: 'right' }}>
{current > 0 && (
<Button onClick={() => setCurrent(current - 1)} style={{ marginRight: 8 }}>
Previous
</Button>
)}
{current < steps.length - 1 && (
<Button type="primary" onClick={() => setCurrent(current + 1)}>
Next
</Button>
)}
{current === steps.length - 1 && (
<Button type="primary" onClick={handleSubmit}>
Submit
</Button>
)}
</div>
</Card>
);
};

Styling Components

Using Ant Design Theme

Global theme customization through ConfigProvider:

// src/App.tsx
import { ConfigProvider } from 'antd';
import { EZApp } from 'ez-console';

export default function App() {
return (
<ConfigProvider
theme={{
token: {
colorPrimary: '#1890ff',
borderRadius: 6,
fontSize: 14,
},
components: {
Button: {
colorPrimary: '#00b96b',
},
Table: {
headerBg: '#fafafa',
},
},
}}
>
<EZApp basePath='/' />
</ConfigProvider>
);
}

Inline Styles

For component-specific styling:

<div style={{
padding: 24,
background: '#fff',
minHeight: 360,
borderRadius: 8,
}}>
Content
</div>

CSS Modules

For more complex styling needs:

// styles.module.css
.container {
padding: 24px;
background: #fff;
border-radius: 8px;
}

.title {
font-size: 20px;
font-weight: 600;
margin-bottom: 16px;
}

// Component.tsx
import styles from './styles.module.css';

<div className={styles.container}>
<h2 className={styles.title}>Title</h2>
Content
</div>

Best Practices

1. Component Composition

Break down complex components into smaller, reusable pieces:

// ✅ Good: Composed components
<ProductCard>
<ProductImage />
<ProductInfo />
<ProductActions />
</ProductCard>

// ❌ Bad: Monolithic component
<ProductCardWithEverything />

2. TypeScript Typing

Always define TypeScript interfaces for component props:

// ✅ Good
interface ProductCardProps {
product: Product;
onEdit?: (product: Product) => void;
onDelete?: (id: string) => void;
}

// ❌ Bad
const ProductCard = (props: any) => { ... }

3. Default Props

Provide sensible defaults:

// ✅ Good
interface ButtonProps {
size?: 'small' | 'medium' | 'large';
variant?: 'primary' | 'secondary';
}

const Button = ({ size = 'medium', variant = 'primary', ...props }: ButtonProps) => {
// ...
};

4. Error Boundaries

Wrap components in error boundaries for better error handling:

import { ErrorBoundary } from 'react-error-boundary';

<ErrorBoundary fallback={<ErrorFallback />}>
<ProductList />
</ErrorBoundary>

5. Loading States

Always show loading indicators during async operations:

const { data, loading } = useRequest(() => apiGet('/products'));

if (loading) {
return <Loading />;
}

return <ProductList data={data} />;

6. Empty States

Handle empty data gracefully:

const { data } = useRequest(() => apiGet('/products'));

if (!data || data.length === 0) {
return <Empty description="No products found" />;
}

return <ProductList data={data} />;

Next Steps


Need help? Ask in GitHub Discussions.