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:

  1. componentDidMount + componentDidUpdate
  2. 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…