Do quite a lot of background management page, almost all table+ popover form. So summarize your CRUD routine
Suppose we want to make a requirement for adding, deleting, modifying and checking articles in Post. Take a look at the final demo sample to see how it works
The type definition
The examples here use typescript 3.7
/** ζη« */
export interface Post {
The primary key * / / * *
id: number
/ * / * * headlines
title: string
/ * * * / content
content: string
/** ηΆζ */
status: PostStatus
/** Sort the fields */
order: number
/** create time, timestamp */
createdAt: number
/** Update time, timestamp */
updatedAt: number
}
/** Article status */
enum PostStatus {
The draft / * * * /
Draft = 0./** Released */
Published = 1,}Copy the code
It is assumed that all back-end interfaces meet the definition of the API type
/** Back-end interface */
export interfaceAPI<Response = unknown> { (... args:any[]) :Promise<Response>
}
Copy the code
Basic data is displayed in pages
First, we need an interface to get a list of articles and to do paging queries. So let’s simulate an interface called getPosts. The type is as follows
export interface GetPostsDto {
/ * *@default 1 * /page? :number
/ * *@default 20 * /pageSize? :number
}
export interface APIPagination {
page: number
total: number
pageSize: number
}
export interface TableListResponse<T = unknown> {
list: T[]
pagination: APIPagination
}
// The type of getPosts
type GetPosts = (dto? : GetPostsDto) = > Promise<TableListResponse<Post>>
Copy the code
Among them, the explanation of the word DTO can refer to Data Transfer Object. I used it because I read the document of NestJS
With interfaces, you can write pages
After installing ANTD, introduce the Table component and define columns in one go
const columns: ColumnProps<Post>[] = [
{ dataIndex: 'id'.title: 'id' },
{ dataIndex: 'title'.title: 'title' },
{ dataIndex: 'content'.title: 'content' },
{ dataIndex: 'status'.title: 'status' },
{ dataIndex: 'order'.title: 'order' },
{ dataIndex: 'createdAt'.title: 'createdAt' },
{ dataIndex: 'updatedAt'.title: 'updatedAt' },
{
render: () = > (
<Space>
<span>The editor</span>
<span style={{ color: 'red' }}>delete</span>
</Space>),},]export default function App() {
return (
<div className='App'>
<h1>antd table curd</h1>
<Table rowKey='id' dataSource={} [] columns={columns}></Table>
</div>)}Copy the code
So the table is rendered
The next step is to figure out how to call the getPosts interface. Let’s revisit the type of getPosts
type GetPosts = (dto? : GetPostsDto) = > Promise<TableListResponse<Post>>
Copy the code
We need a state to store the query parameter GetPostsDto
const [query, setQuery] = React.useState<GetPostsDto>({})
Copy the code
We also need a state to store the data returned by the interface TableListResponse
const [data, setData] = React.useState<TableListResponse<Post>>({
list: [].pagination: {
page: 1.pageSize: 20.total: 0,}})Copy the code
Add a loading
const [loading, setLoading] = React.useState(false)
Copy the code
Then you can call the interface in React. UseEffect
React.useEffect(() = > {
let isCurrent = true
setLoading(true)
getPosts(query)
.then((res) = > isCurrent && setData(res))
.finally(() = > isCurrent && setLoading(false))
return () = > {
// To prevent component setState from being used for unloaded components
isCurrent = false
}
// query calls the interface again each time it changes
}, [query])
Copy the code
In this case, the table already has data that looks something like this
We then need to re-invoke the request to the interface when switching pages. Just listen on the table’s onChange function, setQuery (because query calls the interface again every time it changes)
onChange={(pagination) = > {
setQuery({
page: pagination.current || 1.pageSize: pagination.pageSize || 20
});
}}
Copy the code
Of course, if you only care about pagination changes, you can also listen for the onChange function of the page in the Table component’s pagination configuration, rather than the onChange function of the entire Table
At this point, the basic presentation of the page is complete and can be accessed at codesandbox.io/s/smoosh-ba… To view the online demo
Top search form
Suppose we can do a fuzzy search based on the title of the article, and we need to add an input box above the table
Update the search parameter GetPostsDto as defined by type
export interface GetPostsDto {
/ * *@default 1 * /page? :number
/ * *@default 20 * /pageSize? :numbertitle? :string
}
Copy the code
Then use antD’s Form component to create a business Form component called SearchForm
interfaceFormValues { title? :string
}
export function SearchForm(props: {
onSubmit: (values: FormValues) => any
onReset: (values: FormValues) => any
}) {
const { onSubmit, onReset } = props
const [form] = Form.useForm<FormValues>()
const handleReset = () = > {
form.resetFields()
onReset({ title: undefined})}return (
<Form form={form} layout='inline' onFinish={onSubmit}>
<Form.Item name='title' label='title'>
<Input placeholder='Article Title' />
</Form.Item>
<Button htmlType='submit' type='primary'>search</Button>
<Button htmlType='button' onClick={handleReset}>reset</Button>
</Form>)}Copy the code
We want users to refresh the table whenever they click search or reset. Obviously we need to change the query state again
<SearchForm
onSubmit={(values) = >
setQuery((prev) = >({... prev, ... values,page: 1.// Reset paging
}))
}
onReset={(values) = >
setQuery((prev) = >({... prev, ... values,page: 1.// Reset paging
}))
}
/>
Copy the code
Note here that we use setQuery to pass a function along with the expansion operator to merge this.setState with the update object in the Class Component. See the React documentation
Because we don’t want to click on the search to pass the title argument and throw away arguments like pageSize that might have existed before
In the same way, the onChange function in the table should do the same. You can’t switch pages and lose the title argument that might already exist
onChange={(pagination) = > {
setQuery((prev) = > ({
...prev,
page: pagination.current || 1.pageSize: pagination.pageSize || 20
}));
}}
Copy the code
At this point, the form looks something like thisViewing online DemoCodesandbox. IO/s/great – bla…
Form validation
Now, consider an interesting question. Suppose the title input field, and the user enters an extremely long string, is there any limitation on the front end? Different applications may have different answers
- Like Google’s search box, I tried a maximum of 2048 characters, because it adds the search string to the URL, which is obviously length limited (see implementation), so it makes sense. The search box for Site B does a similar thing, but is limited to 100 characters
- In the user center of Ali Cloud, the front end does not check/filter the length of the input of the order number, but directly throws it to the back end, and then the back end returns the system’s abnormal front popup prompt
- In my usual work, in the background management system, I have written the operation of directly destroying the user and giving the user a hint that the character is too long and so on
Personally, I think it would be better to limit the user’s input directly. For example: “Input fields can enter no more than n strings”, “numeric ID fields can enter only numbers”, “use optional controls instead of input fields (Select, Picker, Select with search, etc.) where appropriate”. Instead of saying that such as the user for illegal input, to do the verification to the user red prompt or pop-up message prompt
Entered, but did not click search
Suppose a user updates the input field, but doesn’t click the search button, and then the user clicks on the next page, etc., should I bring the visually updated title parameter?
After pondering it for a while, I still think it is a user idiot. Why should I bring it if you don’t click search to submit? And if I bring it, should I consider how to deal with form verification? But it all comes down to product choice what’s going on
Filter and sort
Suppose we can filter the table columns by article status and sort them by Post’s order field
Update the search parameter GetPostsDto as defined by type
export interface GetPostsDto {
/ * *@default 1 * /page? :number
/ * *@default 20 * /pageSize? :numbertitle? :string
/** 0 ascending 1 descending */order? :0 | 1status? : PostStatus }Copy the code
As you can see, for the interface, there is no difference except that two fields are added; So for the front end, it doesn’t make any difference, except that the search parameters come from different UI controls, or setQuery to the code
Update the columns status column
{
dataIndex: "status".title: "status".filters: [{text: "0".value: 0 },
{ text: "1".value: 1},].filterMultiple: false,},Copy the code
At the same time, the corresponding table onChange function is updated. At the same time, because antD table is used here, we have to deal with some data structures it gives us, so that it conforms to the specification of the interface
onChange={(pagination, filters) = > {
setQuery((prev) = > ({
...prev,
page: pagination.current || 1.pageSize: pagination.pageSize || 20.status:
filters.status && filters.status.length > 0 ? Number(filters.status[0) :undefined}}})),Copy the code
Sort of the same thing
{ dataIndex: 'order'.title: 'order'.sorter: true },
onChange={(pagination, filters, sorter) = > {
setQuery((prev) = > ({
...prev,
page: pagination.current || 1.pageSize: pagination.pageSize || 20.status:
filters.status && filters.status.length > 0 ? Number(filters.status[0) :undefined.order:!Array.isArray(sorter) && !! sorter.order && sorter.field ==='order'
? ({ ascend: 0.descend: 1 } as const)[sorter.order]
: undefined}}})),Copy the code
And that’s when the data structure gets a little bit gross, and it goes around and around. Well, the data structure that the UI is going to use and the data structure that the interface is going to use are not going to be the same. The same is true for multi-column sorting, where the sorter becomes an array
At this point, the table looks something like this
IO /s/ Async-moo…
Get parameters from the URL to initialize query conditions
Url parameters can be taken from any component, but the url parameters are consumed by the query state, which corresponds to the UI, which could be SearchForm at the top, sorter and filter in the table column, Therefore, it is best to take the URL parameter action directly from the page component, which is app.tsx in the current example
Here install qs library, used for URL querysring parsing and serialization
yarn add qs
yarn add -D @types/qs
Copy the code
Start by writing a function that retrieves the initial query criteria
function getDefaultQuery() {
// Forget about server-side rendering for now
const urlSearchParams = qs.parse(window.location.search, {
ignoreQueryPrefix: true,})const { page, pageSize, title, status, order } = urlSearchParams
const dto: GetPostsDto = {}
if (typeof page === 'string') {
dto.page = validIntOrUndefiend(page) || 1
}
if (typeof pageSize === 'string') {
dto.pageSize = validIntOrUndefiend(pageSize) || 20
}
if (typeof title === 'string') {
dto.title = title
}
if (typeof status === 'string') {
dto.status = validIntOrUndefiend(status)
}
if (typeof order === 'string') {
const orderNum = validIntOrUndefiend(order)
dto.order = orderNum ? (clamp(orderNum, 0.1) as 0 | 1) : undefined
}
return dto
}
Copy the code
Declare an extra state called defaultQuery, initialize it with getDefaultQuery and initialize query with defaultQuery
const [defaultQuery, setDefaultQuery] = React.useState<GetPostsDto>(
getInitialQuery
)
const [query, setQuery] = React.useState<GetPostsDto>(defaultQuery)
Copy the code
Why add another defaultQuery? It is passed to the SearchForm to synchronize the initialized form values
;<SearchForm
defaultQuery={defaultQuery}
//.
/>
export function SearchForm(props: {
onSubmit: (values: FormValues) => any
onReset: (values: FormValues) => anydefaultQuery? : GetPostsDto }) {
const { onSubmit, onReset, defaultQuery } = props
const [form] = Form.useForm<FormValues>()
const handleReset = () = > {
form.resetFields()
onReset({ title: undefined })
}
React.useEffect(() = > {
if(! defaultQuery) {return
}
const { title } = defaultQuery
if (title) {
form.setFieldsValue({ title })
}
}, [form, defaultQuery])
return (
<Form form={form} layout='inline' onFinish={onSubmit}>
<Form.Item name='title' label='title'>
<Input placeholder='Article Title' maxLength={10} />
</Form.Item>
<Button htmlType='submit' type='primary'>search</Button>
<Button htmlType='button' onClick={handleReset}>reset</Button>
</Form>)}Copy the code
For filters and sorters, ANTD columns provide the corresponding controlled attribute, which should be passed in
{
dataIndex: 'status'.title: 'status'.filters: [{text: '0'.value: 0 },
{ text: '1'.value: 1},].filterMultiple: false.filteredValue: query.status === undefined ? undefined : [query.status.toString()],
},
{
dataIndex: 'order'.title: 'order'.sorter: true.sortOrder:
query.order === undefined
? undefined
: ({ 0: 'ascend'.1: 'descend' } as const)[query.order],
},
Copy the code
But it is important to move columns into the App component because columns rely on the Query state and must be placed in order to get the latest Query each time
Synchronize query to URL
The reverse operation synchronizes the Query to the URL every time it changes
React.useEffect(() = > {
const { protocol, host, pathname } = window.location
const newurl = `${protocol}//${host}${pathname}?${qs.stringify(query)}`
window.history.replaceState(null.' ', newurl)
// query synchronizes parameters to the URL for each change
}, [query])
Copy the code
Use the window.history API directly. In real projects, if you use the React-router API, use the React-router API
In fact, I do not do this feature, unless the product explicitly requires otherwise I do not do… But it would be more user friendly
Codesandbox.io /s/cool-cook…
Popup form
Read in CRUD is pretty much done, so let’s look at the additions, deletions, and changes that remain
But before you start, there are now three popovers, and you never know how many popovers the product is going to put under a page
Let’s say a popover corresponds to a visible state and a Modal component, so if we have n popovers, do we have to do this n times?
function Page() {
const [visible1, setVisible1] = React.useState(false)
const [visible2, setVisible2] = React.useState(false)
const [visible3, setVisible3] = React.useState(false)
const [visiblen, setVisiblen] = React.useState(false)
return (
<>
<Modal title='Modal1' visible={visible1}></Modal>
<Modal title='Modal2' visible={visible2}></Modal>
<Modal title='Modal3' visible={visible3}></Modal>
<Modal title='Modaln' visible={visible4}></Modal>
</>)}Copy the code
If each popover had a loading state and so on, the Page would have too many states
But you can’t tell what’s going on if you haven’t written it, so why don’t you write it first
Create
Let’s say we have an interface called createPost with the following type:
type CreatePostDto = {
title: string
content: string
status: PostStatus
order: number
}
type CreatePost = (
dto: CreatePostDto
) = > PromiseThe < {id: number} >Copy the code
According to the documentation in ANTD, you can do a new form in a popover like this, so clang clang clang copy
Create a component called CreateForm
export function CreatForm(props: {
visible: boolean
onCreate: (dto: CreatePostDto) => void
onCancel: () => void
loading: boolean
}) {
const { visible, onCancel, onCreate, loading } = props
const [form] = Form.useForm()
const handleSubmit = () = > {
form.validateFields().then((values) = > {
onCreate(values as CreatePostDto)
})
}
// Reset the form
React.useEffect(() = > {
if(! visible) {return
}
form.resetFields()
}, [visible, form])
return (
<Modal
title='Create Post'
visible={visible}
onCancel={onCancel}
onOk={handleSubmit}
okButtonProps={{ loading }}
>
<Form form={form} labelCol={{ span: 6 }} wrapperCol={{ span: 18}} >
<Form.Item
name='title'
label='title'
rules={[
{
required: true.message: 'title is required'}},] >
<Input></Input>
</Form.Item>
<Form.Item
name='content'
label='content'
rules={[
{
required: true.message: 'content is required'}},] >
<Input.TextArea></Input.TextArea>
</Form.Item>
<Form.Item
name='status'
label='status'
initialValue={PostStatus.Draft}
required
>
<Radio.Group>
<Radio value={PostStatus.Draft}>draft</Radio>
<Radio value={PostStatus.Published}>published</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
name='order'
label='order'
rules={[
{
required: true.message: 'order is required'},]}initialValue={1}
>
<InputNumber min={1}></InputNumber>
</Form.Item>
</Form>
</Modal>)}Copy the code
It is then introduced in the page component and the associated state and binding events are declared
const [createVisible, setCreateVisible] = React.useState(false)
const [createLoading, setCreateLoading] = React.useState(false)
/ /...
<CreatForm
visible={createVisible}
onCreate={async (values: CreatePostDto) => {
setCreateLoading(true)
try {
await createPost(values)
message.success('Created successfully')
// Refresh the list
setQuery((prev) = > ({
...prev,
}))
setCreateVisible(false)}catch (e) {
message.error('Creation failed')}finally {
setCreateLoading(false)
}
}}
onCancel={() = > setCreateVisible(false)}
loading={createLoading}
/>
Copy the code
IO /s/tender-tu…
Note that our interfaces are all emulated and the data is regenerated each time the page is refreshed
Update
Now we’re going to edit, again, the interface type, which we’ll call updatePost
type UpdatePostDto = Partial<Post> & { id: number }
type UpdatePost = (dto: CreatePostDto) = > Promise<void>
Copy the code
Update the article ID is mandatory, other fields are not updated
In general, both our create and edit forms can reuse the same component. We also need the currently edited Post data to initialize the form
Rename CreateForm to PostForm
interface FormValues {
title: string
content: string
status: PostStatus
order: number
}
export function PostForm(props: {
visible: boolean
title: string
loading: boolean
onCancel: () => voidonCreate? : (dto: CreatePostDto) =>voidonUpdate? : (dto: UpdatePostDto) =>voidrecord? : Post }) {
const {
visible,
onCancel,
onCreate,
onUpdate,
loading,
record,
title,
} = props
const [form] = Form.useForm<FormValues>()
const handleSubmit = () = > {
form.validateFields().then((values) = > {
if(record) { onUpdate && onUpdate({ ... values,id: record.id,
} as UpdatePostDto)
} else {
onCreate && onCreate(values as CreatePostDto)
}
})
}
// Initialize the form
React.useEffect(() = > {
form.setFieldsValue({
title: record? .title,content: record? .content,status: record ? record.status : PostStatus.Draft,
order: record? .order ||1,
})
}, [record, form])
return (
<Modal
title={title}
visible={visible}
onCancel={onCancel}
onOk={handleSubmit}
okButtonProps={{ loading }}
>
<Form form={form} labelCol={{ span: 6 }} wrapperCol={{ span: 18}} >
<Form.Item
name='title'
label='title'
rules={[
{
required: true.message: 'title is required'}},] >
<Input></Input>
</Form.Item>
<Form.Item
name='content'
label='content'
rules={[
{
required: true.message: 'content is required'}},] >
<Input.TextArea></Input.TextArea>
</Form.Item>
<Form.Item name='status' label='status' required>
<Radio.Group>
<Radio value={PostStatus.Draft}>draft</Radio>
<Radio value={PostStatus.Published}>published</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
name='order'
label='order'
rules={[
{
required: true.message: 'order is required'}},] >
<InputNumber min={1}></InputNumber>
</Form.Item>
</Form>
</Modal>)}Copy the code
Changes to props: added onUpdate, Record,title, and made onCreate optional
Note also that we are leaving Form initialization to react. useEffect, because the initialValue property of form. Item, like the defaultValue property of uncontrolled input, is useless after the component’s first rendering and will not affect subsequent updates
Then inside the page component, create the same CreateForm as before
<PostForm
title='Update Post'
record={selectedRecord}
visible={updateVisible}
onUpdate={async (values: UpdatePostDto) => {
setUpdateLoading(true)
try {
await updatePost(values)
message.success('Edit succeeded')
// Refresh the list
setQuery((prev) = > ({
...prev,
}))
setUpdateVisible(false)}catch (e) {
message.error('Edit failed')}finally {
setUpdateLoading(false)
}
}}
onCancel={() = > setUpdateVisible(false)}
loading={updateLoading}
/>
Copy the code
Notice that we need a record property here, which is the Post for the current edit, and we need a state declaration to save it
const [selectedRecord, setSelectedRecord] = React.useState<Post>()
Copy the code
When an event is emitted:
{
title: 'operation'.render: (_, record) = > (
<Space>
<span
style={{ cursor: 'pointer'}}onClick={()= >{setSelectedRecord(Record) setUpdateVisible(true)}} > Edit</span>
<span style={{ color: 'red', cursor: 'pointer' }}>delete</span>
</Space>),},Copy the code
Codesandbox.io /s/happy-hai…
Additional interface calls are required for editing
This is also a common requirement, sometimes some extra fields may not be available in the list interface of the table, so you need to call the extra interface to get them. If this is the case, our PostForm props can remain the same, calling the interface based on the information passed in for the current Post, and then setting the value of the form
Delete
Next, delete. Suppose our interface is called deletePost and has the following type:
type DeletePost = (id: number) = > Promise<void>
Copy the code
To delete, we use antD modal.confirm. And onOk returns a Promise to load the button, so that = we don’t have to declare another loading state
function handleDelete(record: Post, onSuccess: () => void) {
Modal.confirm({
title: 'Delete Post'.content: <p>Are you sure to delete {record.title}?</p>,
onOk: async() = > {try {
await deletePost(record.id)
message.success('Deleted successfully')
onSuccess()
} catch (e) {
message.error('Delete failed')}},})}Copy the code
Event binding:
{
title: 'operation'.render: (_, record) = > (
<Space>
<span
style={{ cursor: 'pointer'}}onClick={()= >{setSelectedRecord(Record) setUpdateVisible(true)}} > Edit</span>
<span
style={{ color: 'red', cursor: 'pointer'}}onClick={()= >handleDelete(record, () => setQuery((prev) => { const prevPage = prev.page || 1 return { ... prev, page: data.list.length === 1 ? Clamp (prevPage - 1, 1, prevPage) : prevPage,}})} > Deleted</span>
</Space>),}Copy the code
There is a slight point to note here, that is, the current page only has the last data, if we delete this data and send the original page number, then the user will see the page without data, it will be a bit strange, so we reduce the page number by one page
View the online demo, codesandbox. IO/s/beautiful…
I like modal. confirm as a syntactic sugar, which is handy for operations that don’t require a form to be filled out
The batch operation
Suppose the product tells us that we need a button to publish articles in bulk, then we need an interface to change the status of articles in bulk. Let’s say it’s called batchUpdatePostsStatus
The type definition is as follows:
type BatchUpdatePostsStatusDto = {
ids: number[]
status: PostStatus
}
type BatchUpdatePostsStatus = (dto: BatchUpdatePostsDto) = > Promise<void>
Copy the code
In fact, we can just delete it as before, but in order to make things more complicated, the product said that when batch release, it must add a note. So we’re going to have to do a popover form like we do when we create and edit.
The type of BatchUpdatePostsStatusDto updated to
type BatchUpdatePostsStatusDto = {
ids: number[]
status: PostStatus
/ * * comment * /
remark: string
}
Copy the code
Create a form
interface FormValues {
status: PostStatus
remark: string
}
export function BatchUpdatePostsStatusForm(props: {
visible: boolean
loading: boolean
records: Post[]
onCancel: () => void
onSubmit: (dto: BatchUpdatePostsStatusDto) => Promise<void>}) {
const { visible, onCancel, onSubmit, loading, records } = props
const [form] = Form.useForm<FormValues>()
const handleSubmit = () = > {
form.validateFields().then(async (values) => {
awaitonSubmit({ ... values,ids: records.map((item) = > item.id),
} as BatchUpdatePostsStatusDto)
// Update the reset form
form.resetFields()
})
}
return (
<Modal
title='Batch update article Status'
visible={visible}
onCancel={onCancel}
onOk={handleSubmit}
okButtonProps={{ loading }}
>
<Form form={form} labelCol={{ span: 6 }} wrapperCol={{ span: 18}} >
<Form.Item
name='remark'
label='remark'
rules={[
{
required: true.message: 'remark is required'}},] >
<Input.TextArea placeholder='Fill in the notes'></Input.TextArea>
</Form.Item>
<Form.Item
name='status'
label='status'
required
initialValue={PostStatus.Draft}
>
<Radio.Group>
<Radio value={PostStatus.Draft}>draft 0</Radio>
<Radio value={PostStatus.Published}>published 1</Radio>
</Radio.Group>
</Form.Item>
</Form>
</Modal>)}Copy the code
Add the required states, including multiple rows
const [selectedRows, setSelectedRows] = React.useState<Post[]>([])
const [batchUpdateStatusVisible, setBatchUpdateStatusVisible] = React.useState(
false
)
const [batchUpdateStatusLoading, setBatchUpdateStatusLoading] = React.useState(
false
)
Copy the code
Rendering the form
<BatchUpdatePostsStatusForm
// @see https://ant.design/components/form-cn/#FAQ
forceRender
visible={batchUpdateStatusVisible}
records={selectedRows}
loading={batchUpdateStatusLoading}
onCancel={() = > {
setBatchUpdateStatusVisible(false)
setSelectedRows([])
}}
onSubmit={async (values: BatchUpdatePostsStatusDto) => {
setBatchUpdateStatusLoading(true)
try {
await batchUpdatePostsStatus(values)
message.success('Batch edit succeeded')
// Refresh the list
setQuery((prev) = > ({
...prev,
}))
setBatchUpdateStatusVisible(false)
setSelectedRows([])
} catch (e) {
message.error('Batch edit failed')}finally {
setBatchUpdateStatusLoading(false)}}} / >Copy the code
The binding event
<Button
type='primary'
disabled={selectedRows.length <= 0}
onClick={() = > {
setBatchUpdateStatusVisible(true</Button><Table
rowSelection={{
selectedRowKeys: selectedRows.map((item) = > item.id),
onChange: (_, rows) => setSelectedRows(rows),
}}
/>
Copy the code
Well anyway, is a set, view the online demo, codesandbox. IO/s/proud – dar…
The app.tsx file is about 317 lines long now, so let’s see if we can optimize the way we write it.
Extract the interface to retrieve the data logic externally
Let’s take a look at the current App component
function App() {
const [defaultQuery] = React.useState<GetPostsDto>(getDefaultQuery)
const [query, setQuery] = React.useState<GetPostsDto>(defaultQuery)
const [data, setData] = React.useState<TableListResponse<Post>>({
list: [].pagination: {
page: 1.pageSize: 20.total: 0,}})const [loading, setLoading] = React.useState(false)
const [selectedRecord, setSelectedRecord] = React.useState<Post>()
const [selectedRows, setSelectedRows] = React.useState<Post[]>([])
const [createVisible, setCreateVisible] = React.useState(false)
const [createLoading, setCreateLoading] = React.useState(false)
const [updateVisible, setUpdateVisible] = React.useState(false)
const [updateLoading, setUpdateLoading] = React.useState(false)
const [
batchUpdateStatusVisible,
setBatchUpdateStatusVisible,
] = React.useState(false)
const [
batchUpdateStatusLoading,
setBatchUpdateStatusLoading,
] = React.useState(false)
const columns: ColumnProps<Post>[] = [
{ dataIndex: 'id'.title: 'id' },
{ dataIndex: 'title'.title: 'title' },
{ dataIndex: 'content'.title: 'content' },
{
dataIndex: 'status'.title: 'status'.filters: [{text: '0'.value: 0 },
{ text: '1'.value: 1},].filterMultiple: false.filteredValue:
query.status === undefined ? undefined : [query.status.toString()],
},
{
dataIndex: 'order'.title: 'order'.sorter: true.sortOrder:
query.order === undefined
? undefined
: ({ 0: 'ascend'.1: 'descend' } as const)[query.order],
},
{ dataIndex: 'createdAt'.title: 'createdAt'.sorter: true },
{ dataIndex: 'updatedAt'.title: 'updatedAt' },
{
title: 'operation'.render: (_, record) = > (
<Space>
<span
style={{ cursor: 'pointer'}}onClick={()= >{setSelectedRecord(Record) setUpdateVisible(true)}} > Edit</span>
<span
style={{ color: 'red', cursor: 'pointer'}}onClick={()= >handleDelete(record, () => setQuery((prev) => { const prevPage = prev.page || 1 return { ... prev, page: data.list.length === 1 ? Clamp (prevPage - 1, 1, prevPage) : prevPage,}})} > Deleted</span>
</Space>
),
},
]
React.useEffect(() = > {
let isCurrent = true
setLoading(true)
getPosts(query)
.then((res) = > isCurrent && setData(res))
.finally(() = > isCurrent && setLoading(false))
return () = > {
// To prevent component setState from being used for unloaded components
isCurrent = false
}
// query calls the interface again each time it changes
}, [query])
React.useEffect(() = > {
const { protocol, host, pathname } = window.location
const newurl = `${protocol}//${host}${pathname}?${qs.stringify(query)}`
window.history.replaceState(null.' ', newurl)
// query synchronizes parameters to the URL for each change
}, [query])
return (
<div>
<h1>antd table crud</h1>
<SearchForm
defaultQuery={defaultQuery}
onSubmit={(values)= >setQuery((prev) => ({ ... prev, ... Values, page: 1, / / reset paging onReset = {})} (values) = > setQuery ((prev) = > ({... prev, ... Values, page: 1, // reset page}))} /><div style={{ margin: '15px 0' }}>
<Space>
<Button type='primary' onClick={()= > setCreateVisible(true)}>
Create
</Button>
<Button
type='primary'
disabled={selectedRows.length< =0}
onClick={()= >{setBatchUpdateStatusVisible (true)}} > batch update status</Button>
</Space>
</div>
<Table
rowKey='id'
dataSource={data.list}
columns={columns}
loading={loading}
pagination={{ . antdPaginationAdapter(data.pagination)}}onChange={(pagination, filters.sorter) = >{ setQuery((prev) => ({ ... prev, page: pagination.current || 1, pageSize: pagination.pageSize || 20, status: filters.status && filters.status.length > 0 ? Number(filters.status[0]) : undefined, order: ! Array.isArray(sorter) && !! sorter.order && sorter.field === 'order' ? ({ ascend: 0, descend: 1 } as const)[sorter.order] : undefined, })) }} rowSelection={{ selectedRowKeys: selectedRows.map((item) => item.id), onChange: (_, rows) => setSelectedRows(rows), }} ></Table>
<PostForm
title='Create Post'
visible={createVisible}
onCreate={async (values: CreatePostDto) = >{setCreateLoading(true) try {await createPost(values) message. Success (' create success ') // Refresh list setQuery((prev) => ({... prev, })) setCreateVisible(false)} Catch (e) {message.error(' create failed ')} finally {setCreateLoading(false)}}} onCancel={() => setCreateVisible(false)} loading={createLoading} /><PostForm
title='Update Post'
record={selectedRecord}
visible={updateVisible}
onUpdate={async (values: UpdatePostDto) = >{setUpdateLoading(true) try {await updatePost(values) message. Success (' edit successfully ') setQuery((prev) => ({... prev, })) setUpdateVisible(false)} Catch (e) {message.error(' edit failed ')} finally {setUpdateLoading(false)}} onCancel={() => setUpdateVisible(false)} loading={updateLoading} /><BatchUpdatePostsStatusForm
visible={batchUpdateStatusVisible}
records={selectedRows}
loading={batchUpdateStatusLoading}
onCancel={()= >{ setBatchUpdateStatusVisible(false) setSelectedRows([]) }} onSubmit={async (values: BatchUpdatePostsStatusDto) => { setBatchUpdateStatusLoading(true) try { await batchUpdatePostsStatus(values) Message. Success (' batch edit succeeded ') // Refresh the list setQuery((prev) => ({... prev, })) setBatchUpdateStatusVisible (false) setSelectedRows ([])} the catch (e) {message. Error (' batch edit failure ')} finally { setBatchUpdateStatusLoading(false) } }} /></div>)}Copy the code
First of all, the method of retrieving table data from Query is very fixed. We can extract it outside the App component and form a function called usePosts
function usePosts(defaultQuery: GetPostsDto) {
const [query, setQuery] = React.useState<GetPostsDto>(defaultQuery)
const [data, setData] = React.useState<TableListResponse<Post>>({
list: [].pagination: {
page: 1.pageSize: 20.total: 0,}})const [loading, setLoading] = React.useState(false)
React.useEffect(() = > {
let isCurrent = true
setLoading(true)
getPosts(query)
.then((res) = > isCurrent && setData(res))
.finally(() = > isCurrent && setLoading(false))
return () = > {
// To prevent component setState from being used for unloaded components
isCurrent = false
}
// query calls the interface again each time it changes
}, [query])
return {
query,
setQuery,
data,
loading,
}
}
Copy the code
Then, delete the relevant code in the App component and replace it with this sentence
const { data, query, setQuery, loading } = usePosts(defaultQuery)
Copy the code
But if so, our usePosts can only be used for adding, deleting, changing and checking articles. The data structures of the interfaces should be consistent within the same project. If you look at usePosts up there, where there’s a type notation, it’s already telling you how to abstract, so you need to use generics
export function useTableListQuery<
Query extends { page? :number; pageSize? :number },
Entity
>(
api: (query: Query) = > Promise<TableListResponse<Entity>>,
defaultQuery: Query
) {
const [query, setQuery] = React.useState<Query>(defaultQuery)
const [data, setData] = React.useState<TableListResponse<Entity>>({
list: [].pagination: {
page: 1.pageSize: 20.total: 0,}})const [loading, setLoading] = React.useState(false)
React.useEffect(() = > {
let isCurrent = true
setLoading(true)
api(query)
.then((res) = > isCurrent && setData(res))
.finally(() = > isCurrent && setLoading(false))
return () = > {
// To prevent component setState from being used for unloaded components
isCurrent = false
}
// query calls the interface again each time it changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query])
return {
query,
setQuery,
data,
loading,
}
}
Copy the code
Here we extract a function called useTableListQuery, which takes two parameters: the function that calls the back-end interface and the default query parameter. There is no logical difference from usePosts
Then modify the relevant code in the App component as follows: π
const { data, query, setQuery, loading } = useTableListQuery(
getPosts,
defaultQuery
)
Copy the code
Similarly, the logic of synchronizing query state to URL parameters can be extracted externally:
export function useStateSyncToUrl<T> (state: T, options? : qs.IStringifyOptions) {
const optionsRef = React.useRef(options)
React.useEffect(() = > {
const { protocol, host, pathname } = window.location
const newurl = `${protocol}//${host}${pathname}?${qs.stringify( state, optionsRef.current )}`
window.history.replaceState(null.' ', newurl)
// Synchronize the parameters to the URL each time the state changes
}, [state])
}
Copy the code
View the online demo, codesandbox. IO/s/infallibl…
Abstract popover form logic
As we write along, we can see that the logic of these popover forms is similar: Click the button -> popover -> fill in the form -> submit interface -> interface call success -> refresh the table data popover, since the routine is more unified, I think it can and is worth abstract, of course, if there are exceptions to the special processing is good
Unified visible and Loading states
The interaction like popover interrupts the user’s other operations and allows the user to focus on the popover itself. Therefore, one of the visible states above can be used at the same time
We have three businesses/operations: create, edit, batch update post status, and define a type for them
type ModalActionType = ' ' | 'create' | 'update' | 'batchUpdateStatus'
Copy the code
Where an empty string indicates that no operation is performed
Next, define the associated state:
const [modalActionType, setModalActionType] = React.useState<ModalActionType>(
' '
)
Copy the code
In this case, determine whether creating a popover form display works
visible = {modalActionType === 'create'}
Copy the code
Open the Create popover form to use
setModalActionType('create')
Copy the code
Close popover to work
setModalActionType(' ')
Copy the code
Editing is the same as batch updating the status of articles
Then loading, of course, can be done as visible above, but I don’t think it is necessary, just use one directly:
const [modalActionLoading, setModalActionLoading] = React.useState(false)
Copy the code
Ok, cancel the event binding of the button
To determine the button’s event binding, the main difference is the interface to call; The second thing is that after the interface is successful, the operation may be different, for example, if you have a multi-select table, and you want to get rid of the multi-select record, you can try to write a factory function that is uniformly invariant, mutable and passed by parameters
type ModalActionFactory = <
API extends(... args:any[]) = >Promise<unknown>
>(options: { api: API successMessage? :stringerrorMessage? :string
}) = > (. args: Parameters
) = > Promise<void>
const clean = () = > {
setSelectedRecord(undefined)
setSelectedRows([])
}
const handleModalCancel = () = > {
setModalActionType(' ')
clean()
}
const modalActionFactory: ModalActionFactory = (options) = > {
const {
api,
successMessage = 'Operation successful',
errorMessage = 'Operation failed',
} = options
return async(... args:any[]) => {
setModalActionLoading(true)
try {
await api(args)
message.success(successMessage)
// Refresh the list
setQuery((prev) = > ({
...prev,
}))
handleModalCancel()
} catch (e) {
message.error(errorMessage)
} finally {
setModalActionLoading(false)}}}Copy the code
As you can see above, modalActionFactory takes the parameters of the interface API and returns a function that adds the processing logic for the success and failure of the interface call. In this way, the component can say:
<PostForm
title='Create Post'
visible={modalActionType === 'create'}
onCreate={modalActionFactory({
api: createPost,
successMessage: 'Created successfully'.errorMessage: 'Creation failed',
})}
onCancel={handleModalCancel}
loading={modalActionLoading}
/>
Copy the code
This way, it will look more uniform and write less template code. The downside is that if there are special cases where the modalActionFactory wrapper might not work, I recommend writing it separately
IO /s/jolly-eul…
Hooks vs Class Components
You can see the examples above, which are written in hooks. It’s 2021, I don’t want to tangle about which one is better, which one will be used soon after work
Reusing hooks logic has the advantage, class code organization makes people feel more organized and neat, that’s my feeling
If the above example is written in class, the key point is how to do it:
React.useEffect(() = > {
let isCurrent = true
setLoading(true)
api(query)
.then((res) = > isCurrent && setData(res))
.finally(() = > isCurrent && setLoading(false))
return () = > {
// To prevent component setState from being used for unloaded components
isCurrent = false
}
// query calls the interface again each time it changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query])
Copy the code
I can think of two:
componentDidMount + componentDidUpdate
this.setState({ query }, () => api.then(() => {/** logic */}))
Second, if I write in class, I rarely want to extract use-table-list-query logic, preferring to write once on every page. Because if you extract logic like this, you’re probably just using hoc, hoc, and you don’t really want to use it
Hooks use directly is much more intuitive, but if you use too much, or if a function component internally defines a large number of variables/subfunctions such as const XXX = yyy, the code structure can look messy. It’s the same old saying, whichever one gets off the clock
reference
- ant.design/index-cn
- pro.ant.design/index-cn
- Reactjs.org/docs/hooks-…
- ahooks.js.org/
- Cmichel.medium.com/how-to-depl…