# CollectionOptimisticActions

**Category:** Features/Actions/Updates

## Design

### Description

The CollectionOptimisticActions class provides a set of utilities for managing optimistic actions on a collection of data. It allows performing various actions such as adding, updating, and deleting items from a collection while assuming that these actions will succeed, even before confirming with the server.

You can learn about collection approches for [Updates](./?path=/story/features-actions-updates--updates)

To manage a collection using CollectionOptimisticActions use the hook [useOptimisticActions](./?path=/story/features-actions-updates--useoptimisticactions)

#### Error Handling
Offers hooks for handling errors, retrying actions, and updating UI based on the success or failure of optimistic actions.

#### Rollback
Optimistic Actions come with built-in rollback functionality to handle errors gracefully. When using any of the provided APIs, if the submit function returns a rejected promise indicating an error, the changes made to the collection will automatically be reverted.

#### Dirty UI State
In the context of optimistic updates, the term "dirty state" refers to the fact that the UI has been updated optimistically based on user input, but the changes have not yet been confirmed by the server. The UI is in a "dirty" state until the server response is received and the changes are reconciled.
The user navigates away or closes the page before the server confirms the changes
Therefore, optimistic updates should not be used for critical or irreversible actions where data integrity is paramount.

#### Server Simulation
To reflect changes correctly to the collection until the server finishes the update, WixPatterns simulates the eventual collection state after the update.
 This simulation entails the following:

- Updating relevant columns or properties of the items displayed in the UI.
- Hiding updated items that no longer meet the criteria of the current applied filtering.
- Revealing updated items that now satisfy the criteria of the current applied filtering.


The simulation is done by the `predicate` & `orderBy` parameters. 
WixPatterns includes default implementations for these parameters, which are designed based on assumptions about the typical behavior of servers.
Therefore, it's crucial to ensure consistency between client-side and server-side filtering, either by using the default implementations or by explicitly implementing the `predicate` & `orderBy` functions.

> When not using [Sortable Columns](./?path=/story/features-sort-sortable-columns-sortable-columns--sortable-columns) it's necessary to specify the `orderBy` parameter; otherwise, WixPatterns will not merge the updated items when they should be visible under a new filtering criteria. 
***The default implementations are not guaranteed to be compatible with all servers.***
***Hence, ensure consistency in filtering behavior across both client and server sides by explicitly implementing the `predicate` and `orderBy` functions when necessary.***


### Examples

### Create Methods

Allows for adding items to the collection, even if their id is not set.
 Please note the following:
1. If you attempt to perform a sequence of actions, starting with create followed by update or delete on the same item, and you call the second action before the item has been created on the server, the update or delete operation will fail. 
2. The function requires two arguments: the first being the optimistically created item(s), and the second being a params object that must include an onSubmit function. The type of the item(s) in the first argument must match the return type of the onSubmit function, which should include the id of the created item(s).

### Create one

Create one item optimistically. 
 In this example, click on "Create contact" to see a new contact. 
 The item received in the submit function is temporary (can be without id which created in the server). Ensure you return the created item with all the properties from the server response. This will be the item in the collection at the end of this operation.

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  Table,
  OffsetQuery,
  PrimaryActionButton,
  useTableCollection,
  useOptimisticActions,
} from '@wix/patterns';
import { contacts } from '@wix/crm';

function CreateOne() {
  const state = useTableCollection<contacts.Contact>({
    queryName: 'contacts-CreateOne',
    itemName: (item) => `${item.info?.name?.first} ${item.info?.name?.last}`,
    fetchData: (query: OffsetQuery) => {
      const { limit, offset, search, filters } = query;

      let queryBuilder = contacts.queryContacts().limit(limit).skip(offset);

      if (search) {
        queryBuilder = queryBuilder.startsWith('info.name.first', search);
      }

      return queryBuilder.find().then(({ items = [], totalCount: total }) => ({
        items,
        total,
      }));
    },
    fetchErrorMessage: () => 'Error fetching contacts',
  });

  const collection = state.collection;

  const optimisticActions = useOptimisticActions(collection);

  const _createContact = () => {
    const newContact = {
      id: new window.Date().toString(),
      info: {
        name: {
          first: 'New',
          last: 'Contact',
        },
      },
    };

    optimisticActions.createOne(newContact, {
      submit: async (_contacts) => {
        const response = (await Promise.all(
          _contacts.map((contact) =>
            contacts.createContact(contact.info as contacts.ContactInfo),
          ),
        )) as contacts.CreateContactResponse[];

        return response.map(({ contact }) => contact as contacts.Contact);
      },
    });
  };

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          state={collection}
          primaryActionButton={
            <PrimaryActionButton onClick={_createContact}>
              Add Contact
            </PrimaryActionButton>
          }
          columns={[
            {
              title: '',
              width: '50px',
              render: (contact) => (
                <Avatar
                  name={`${contact.info?.name?.first} ${contact.info?.name?.last}`}
                  imgProps={{ src: contact.info?.picture?.image }}
                />
              ),
            },
            {
              title: 'Name',
              width: '250px',
              render: (contact) =>
                `${contact.info?.name?.first} ${contact.info?.name?.last}`,
            },
            {
              title: 'Level',
              render: (contact) => contact.info?.jobTitle,
            },
            {
              title: 'Last Activity',
              render: (contact) =>
                contact.lastActivity?.activityDate?.toLocaleString(),
            },
          ]}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

### Create many

Create many items optimatically.
 In this example, click on "Create contacts" to see several new contacts. Scroll down to see the new contacts

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  Table,
  OffsetQuery,
  PrimaryActionButton,
  useTableCollection,
  useOptimisticActions,
} from '@wix/patterns';
import { contacts } from '@wix/crm';

function CreateMany() {
  const state = useTableCollection<contacts.Contact>({
    queryName: 'contacts-CreateMany',
    itemName: (item) => `${item.info?.name?.first} ${item.info?.name?.last}`,
    fetchData: (query: OffsetQuery) => {
      const { limit, offset, search, filters } = query;

      let queryBuilder = contacts.queryContacts().limit(limit).skip(offset);

      if (search) {
        queryBuilder = queryBuilder.startsWith('info.name.first', search);
      }

      return queryBuilder.find().then(({ items = [], totalCount: total }) => ({
        items,
        total,
      }));
    },
    fetchErrorMessage: () => 'Error fetching contacts',
  });

  const collection = state.collection;

  const optimisticActions = useOptimisticActions(collection);

  const generateContacts = () =>
    Array.from({ length: 5 }, (_, index) => ({
      id: generateKey(index),
      info: {
        name: {
          first: 'Contact',
          last: generateKey(index),
        },
      },
    }));

  const generateKey = (index: number): string =>
    new window.Date().toString() + index;

  const _createContacts = () => {
    const newContacts = generateContacts();

    optimisticActions.createMany(newContacts, {
      submit: async (_contacts) => {
        const response = (await Promise.all(
          _contacts.map((contact) =>
            contacts.createContact(contact.info as contacts.ContactInfo),
          ),
        )) as contacts.CreateContactResponse[];

        return response.map(({ contact }) => contact as contacts.Contact);
      },
    });
  };

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          state={collection}
          primaryActionButton={
            <PrimaryActionButton onClick={_createContacts}>
              Add contacts
            </PrimaryActionButton>
          }
          columns={[
            {
              title: '',
              width: '50px',
              render: (contact) => (
                <Avatar
                  name={`${contact.info?.name?.first} ${contact.info?.name?.last}`}
                  imgProps={{ src: contact.info?.picture?.image }}
                />
              ),
            },
            {
              title: 'Name',
              width: '250px',
              render: (contact) =>
                `${contact.info?.name?.first} ${contact.info?.name?.last}`,
            },
            {
              title: 'Level',
              render: (contact) => contact.info?.jobTitle,
            },
            {
              title: 'Last Activity',
              render: (contact) =>
                contact.lastActivity?.activityDate?.toLocaleString(),
            },
          ]}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

---

### Update Methods

Allows updating items in the collection while submitting the update action to the server and immediately reflecting the change on the screen - either by changing some property, or by filtering out the updated item.

### Update one

Update one item in the table. 
 In this example, click on update button to see the new level of contact

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  Table,
  OffsetQuery,
  useTableCollection,
  useOptimisticActions,
} from '@wix/patterns';
import { contacts } from '@wix/crm';

function UpdateOne() {
  const state = useTableCollection<contacts.Contact>({
    queryName: 'contacts-UpdateOne',
    itemName: (item) => `${item.info?.name?.first} ${item.info?.name?.last}`,
    fetchData: (query: OffsetQuery) => {
      const { limit, offset, search, filters } = query;

      let queryBuilder = contacts.queryContacts().limit(limit).skip(offset);

      if (search) {
        queryBuilder = queryBuilder.startsWith('info.name.first', search);
      }

      return queryBuilder.find().then(({ items = [], totalCount: total }) => ({
        items,
        total,
      }));
    },
    fetchErrorMessage: () => 'Error fetching contacts',
  });

  const collection = state.collection;

  const optimisticActions = useOptimisticActions(collection);

  const _updateContact = (contact: contacts.Contact) => {
    const updatedContact = {
      ...contact,
      info: {
        ...contact.info,
        jobTitle: 'Ultimate',
      },
    };

    optimisticActions.updateOne(updatedContact, {
      submit: async (_contacts) => {
        const response = (await Promise.all(
          _contacts.map((contact) =>
            contacts.updateContact(
              contact._id as string,
              contact.info as contacts.ContactInfo,
              contact.revision as number,
            ),
          ),
        )) as contacts.UpdateContactResponse[];

        return response.map(({ contact }) => contact as contacts.Contact);
      },
    });
  };

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          state={collection}
          columns={[
            {
              title: '',
              width: '50px',
              render: (contact) => (
                <Avatar
                  name={`${contact.info?.name?.first} ${contact.info?.name?.last}`}
                  imgProps={{ src: contact.info?.picture?.image }}
                />
              ),
            },
            {
              title: 'Name',
              width: '250px',
              render: (contact) =>
                `${contact.info?.name?.first} ${contact.info?.name?.last}`,
            },
            {
              title: 'Level',
              render: (contact) => contact.info?.jobTitle,
            },
            {
              title: 'Last Activity',
              render: (contact) =>
                contact.lastActivity?.activityDate?.toLocaleString(),
            },
          ]}
          actionCell={(contact) => ({
            primaryAction: {
              text: 'Update to Ultimate',
              onClick: () => {
                _updateContact(contact);
              },
            },
          })}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

### Update many

Update many items optimatically by using bulk selection.
 In this example, click on all items and see only 2 items are selected because we use `maxSelection`, and only these 2 are been updated

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  BulkActionButton,
  BulkActionToolbar,
  Table,
  OffsetQuery,
  useTableCollection,
  useOptimisticActions,
} from '@wix/patterns';
import { contacts } from '@wix/crm';

function UpdateMany() {
  const state = useTableCollection<contacts.Contact>({
    queryName: 'contacts-UpdateMany',
    itemName: (item) => `${item.info?.name?.first} ${item.info?.name?.last}`,
    fetchData: (query: OffsetQuery) => {
      const { limit, offset, search, filters } = query;

      let queryBuilder = contacts.queryContacts().limit(limit).skip(offset);

      if (search) {
        queryBuilder = queryBuilder.startsWith('info.name.first', search);
      }

      return queryBuilder.find().then(({ items = [], totalCount: total }) => ({
        items,
        total,
      }));
    },
    fetchErrorMessage: () => 'Error fetching contacts',
  });

  const collection = state.collection;

  const optimisticActions = useOptimisticActions(collection);

  const _updateContacts = (contactsToUpdate: contacts.Contact[]) => {
    const updatedContacts = contactsToUpdate.map((c) => ({
      ...c,
      info: {
        ...c.info,
        jobTitle: 'Ultimate',
      },
    }));

    optimisticActions.updateMany(updatedContacts, {
      successToast: `${contactsToUpdate.length} contacts updated`,
      submit: async (_contacts) => {
        const response = (await Promise.all(
          _contacts.map((contact) =>
            contacts.updateContact(
              contact._id as string,
              contact.info as contacts.ContactInfo,
              contact.revision as number,
            ),
          ),
        )) as contacts.UpdateContactResponse[];

        return response.map(({ contact }) => contact as contacts.Contact);
      },
    });
  };

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          state={collection}
          columns={[
            {
              title: '',
              width: '50px',
              render: (contact) => (
                <Avatar
                  name={`${contact.info?.name?.first} ${contact.info?.name?.last}`}
                  imgProps={{ src: contact.info?.picture?.image }}
                />
              ),
            },
            {
              title: 'Name',
              width: '250px',
              render: (contact) =>
                `${contact.info?.name?.first} ${contact.info?.name?.last}`,
            },
            {
              title: 'Level',
              render: (contact) => contact.info?.jobTitle,
            },
            {
              title: 'Last Activity',
              render: (contact) =>
                contact.lastActivity?.activityDate?.toLocaleString(),
            },
          ]}
          showSelection
          maxSelection={2}
          bulkActionToolbar={({ selectedValues, clearSelection, total }) => (
            <BulkActionToolbar
              actionButton={
                <BulkActionButton
                  onClick={() => {
                    _updateContacts(selectedValues);
                    clearSelection();
                  }}
                >
                  Update to Ultimate
                </BulkActionButton>
              }
            />
          )}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

### Update all

Update all items optimatically by using bulk selection.
 `updateAll` is for the use case when all items that match the current filtering should be updated, but naturally not all of them are currently visible in the table (because of pagination)
 In this example, click one time on several items and one time on all items and see the updated items

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  BulkActionButton,
  BulkActionToolbar,
  Table,
  OffsetQuery,
  useTableCollection,
  useOptimisticActions,
} from '@wix/patterns';
import {
  bulkUpdateContacts,
  queryContacts,
  updateContact,
} from '@wix/ambassador-contacts-v4-contact/http';
import {
  Contact,
  ContactInfo,
  UpdateContactRequest,
} from '@wix/ambassador-contacts-v4-contact/types';
import { useHttpClient } from '@wix/yoshi-flow-bm';

function UpdateAll() {
  const httpClient = useHttpClient();

  const state = useTableCollection<Contact>({
    queryName: 'contacts-UpdateAll',
    fetchData: (query: OffsetQuery) => {
      const { limit, offset, search, filters } = query;
      return httpClient
        .request(
          queryContacts({
            query: { paging: { limit, offset }, filter: filters },
            search,
          }),
        )
        .then(({ data: { contacts = [], pagingMetadata } }) => ({
          items: contacts,
          total: pagingMetadata?.total,
        }));
    },
    itemKey: (item) => item.id as string,
    itemName: (item) => item.name as string,
    fetchErrorMessage: () => 'Error fetching contacts',
  });

  const collection = state.collection;

  const optimisticActions = useOptimisticActions(collection);

  const _updateContacts = (
    contacts: Contact[],
    { allSelected, total }: { allSelected: boolean; total: number },
  ) => {
    if (allSelected) {
      const newLevelContact = { level: 'Legendary' } as Partial<Contact>;
      optimisticActions.updateAll(newLevelContact, {
        successToast: `${total} contacts changed`,
        submit: () =>
          httpClient.request(
            bulkUpdateContacts({
              info: newLevelContact as ContactInfo,
            }),
          ),
      });
    } else {
      const updatedContacts = contacts.map((c) => ({
        ...c,
        level: 'Legendary',
      }));

      optimisticActions.updateMany(updatedContacts, {
        successToast: `${contacts.length} contacts updated`,
        submit: (contacts) =>
          Promise.all(
            contacts.map((contact) =>
              httpClient.request(
                updateContact(contact as UpdateContactRequest),
              ),
            ),
          ),
      });
    }
  };

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          state={collection}
          columns={[
            {
              title: '',
              width: '50px',
              render: (contact) => (
                <Avatar name={contact.name} imgProps={{ src: contact.image }} />
              ),
            },
            {
              title: 'Name',
              width: '250px',
              render: (contact) => contact.name,
            },
            {
              title: 'Level',
              render: (contact) => contact.level,
            },
            {
              title: 'Created Date',
              render: (contact) => contact.createdDate?.toDateString(),
            },
          ]}
          showSelection
          bulkActionToolbar={({
            selectedValues,
            clearSelection,
            allSelected,
            total,
          }) => (
            <BulkActionToolbar
              actionButton={
                <BulkActionButton
                  onClick={() => {
                    _updateContacts(selectedValues, {
                      allSelected,
                      total,
                    });
                    clearSelection();
                  }}
                >
                  Update to Legendary
                </BulkActionButton>
              }
            />
          )}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

---

### Delete Methods

Allows removing items from the collection while submitting the delete action to the server, and immediately reflect the deletion to the screen.

### Delete one

Delete one item optimatically.

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  Table,
  OffsetQuery,
  useTableCollection,
  useOptimisticActions,
} from '@wix/patterns';
import { contacts } from '@wix/crm';

function DeleteOne() {
  const state = useTableCollection<contacts.Contact>({
    queryName: 'contacts-DeleteOne',
    itemName: (item) => `${item.info?.name?.first} ${item.info?.name?.last}`,
    fetchData: (query: OffsetQuery) => {
      const { limit, offset, search, filters } = query;

      let queryBuilder = contacts.queryContacts().limit(limit).skip(offset);

      if (search) {
        queryBuilder = queryBuilder.startsWith('info.name.first', search);
      }

      return queryBuilder.find().then(({ items = [], totalCount: total }) => ({
        items,
        total,
      }));
    },
    fetchErrorMessage: () => 'Error fetching contacts',
  });

  const collection = state.collection;

  const optimisticActions = useOptimisticActions(collection);

  const _deleteContact = (contact: contacts.Contact) => {
    optimisticActions.deleteOne(contact, {
      submit: async (_contacts) =>
        Promise.all(
          _contacts.map((contact) =>
            contacts.deleteContact(contact._id as string),
          ),
        ),
    });
  };

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          state={collection}
          columns={[
            {
              title: '',
              width: '50px',
              render: (contact) => (
                <Avatar
                  name={`${contact.info?.name?.first} ${contact.info?.name?.last}`}
                  imgProps={{ src: contact.info?.picture?.image }}
                />
              ),
            },
            {
              title: 'Name',
              width: '250px',
              render: (contact) =>
                `${contact.info?.name?.first} ${contact.info?.name?.last}`,
            },
            {
              title: 'Level',
              render: (contact) => contact.info?.jobTitle,
            },
            {
              title: 'Last Activity',
              render: (contact) =>
                contact.lastActivity?.activityDate?.toLocaleString(),
            },
          ]}
          showSelection
          actionCell={(contact) => ({
            primaryAction: {
              text: 'Delete',
              onClick: () => {
                _deleteContact(contact);
              },
            },
          })}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

### Delete many

Delete many items optimatically by using bulk selection.

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  BulkActionButton,
  BulkActionToolbar,
  Table,
  OffsetQuery,
  useTableCollection,
  useOptimisticActions,
} from '@wix/patterns';
import { contacts } from '@wix/crm';

function DeleteMany() {
  const state = useTableCollection<contacts.Contact>({
    queryName: 'contacts-DeleteMany',
    itemName: (item) => `${item.info?.name?.first} ${item.info?.name?.last}`,
    fetchData: (query: OffsetQuery) => {
      const { limit, offset, search, filters } = query;

      let queryBuilder = contacts.queryContacts().limit(limit).skip(offset);

      if (search) {
        queryBuilder = queryBuilder.startsWith('info.name.first', search);
      }

      return queryBuilder.find().then(({ items = [], totalCount: total }) => ({
        items,
        total,
      }));
    },
    fetchErrorMessage: () => 'Error fetching contacts',
  });

  const collection = state.collection;

  const optimisticActions = useOptimisticActions(collection);

  const _deleteContacts = (_contacts: contacts.Contact[]) => {
    optimisticActions.deleteMany<void[]>(_contacts, {
      successToast: `${_contacts.length} contacts deleted`,
      submit: async (contactsToDelete) =>
        Promise.all(
          contactsToDelete.map((contact) =>
            contacts.deleteContact(contact._id as string),
          ),
        ),
    });
  };

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          state={collection}
          columns={[
            {
              title: '',
              width: '50px',
              render: (contact) => (
                <Avatar
                  name={`${contact.info?.name?.first} ${contact.info?.name?.last}`}
                  imgProps={{ src: contact.info?.picture?.image }}
                />
              ),
            },
            {
              title: 'Name',
              width: '250px',
              render: (contact) =>
                `${contact.info?.name?.first} ${contact.info?.name?.last}`,
            },
            {
              title: 'Level',
              render: (contact) => contact.info?.jobTitle,
            },
            {
              title: 'Last Activity',
              render: (contact) =>
                contact.lastActivity?.activityDate?.toLocaleString(),
            },
          ]}
          showSelection
          maxSelection={2}
          bulkActionToolbar={({ selectedValues, clearSelection }) => (
            <BulkActionToolbar
              actionButton={
                <BulkActionButton
                  onClick={() => {
                    _deleteContacts(selectedValues);
                    clearSelection();
                  }}
                >
                  Delete
                </BulkActionButton>
              }
            />
          )}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

### Delete all

Delete all items optimatically by using bulk selection. 

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  BulkActionButton,
  BulkActionToolbar,
  Table,
  OffsetQuery,
  useTableCollection,
  useOptimisticActions,
} from '@wix/patterns';
import {
  deleteContact,
  queryContacts,
  bulkDeleteContacts,
} from '@wix/ambassador-contacts-v4-contact/http';
import { Contact } from '@wix/ambassador-contacts-v4-contact/types';
import { useHttpClient } from '@wix/yoshi-flow-bm';

function DeleteAll() {
  const httpClient = useHttpClient();

  const state = useTableCollection<Contact>({
    queryName: 'contacts-DeleteAll',
    fetchData: (query: OffsetQuery) => {
      const { limit, offset, search, filters } = query;
      return httpClient
        .request(
          queryContacts({
            query: { paging: { limit, offset }, filter: filters },
            search,
          }),
        )
        .then(({ data: { contacts = [], pagingMetadata } }) => ({
          items: contacts,
          total: pagingMetadata?.total,
        }));
    },
    itemKey: (item) => item.id as string,
    itemName: (item) => item.name as string,
    fetchErrorMessage: () => 'Error fetching contacts',
  });

  const collection = state.collection;

  const optimisticActions = useOptimisticActions(collection);

  const _deleteContacts = (
    contacts: Contact[],
    { allSelected, total }: { allSelected: boolean; total: number },
  ) => {
    if (allSelected) {
      optimisticActions.deleteAll({
        successToast: `${total} contacts deleted`,
        submit: () => {
          try {
            return httpClient.request(bulkDeleteContacts({} as any));
          } catch (error) {
            console.log(error);
          }

          return Promise.resolve() as any;
        },
      });
    } else {
      optimisticActions.deleteMany(contacts, {
        successToast: `${contacts.length} contacts deleted`,
        submit: (contactsToDelete) =>
          Promise.all(
            contactsToDelete.map((contactToDelete) =>
              deleteContact({
                contactId: contactToDelete.id as string,
              }),
            ),
          ),
      });
    }
  };

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          state={collection}
          columns={[
            {
              title: '',
              width: '50px',
              render: (contact) => (
                <Avatar name={contact.name} imgProps={{ src: contact.image }} />
              ),
            },
            {
              title: 'Name',
              width: '250px',
              render: (contact) => contact.name,
            },
            {
              title: 'Level',
              render: (contact) => contact.level,
            },
            {
              title: 'Last Seen',
              render: (contact) => contact.lastSeen?.toLocaleString(),
            },
          ]}
          showSelection
          bulkActionToolbar={({
            selectedValues,
            clearSelection,
            allSelected,
            total,
          }) => (
            <BulkActionToolbar
              actionButton={
                <BulkActionButton
                  onClick={() => {
                    _deleteContacts(selectedValues, { allSelected, total });
                    clearSelection();
                  }}
                >
                  Delete
                </BulkActionButton>
              }
            />
          )}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

## API

### Method Params

The params contains the following part of these props:

- **showUndoToast?:** Determines whether to display an undo toast before calling the server action. Default value is `false`.
- **showUndoSuccessToast?:** Determines whether to display an undo button in the success toast. Default value is `false`.
- **onUndo?:** Callback function triggered when the user clicks on the undo button.
- **errorToast?:** Function to generate error toast to show in case the server fails to perform the action.
- **onError?:** Callback function triggered when an error occurs.
- **onTryAgain?:** Callback function to retry the action.
- **successToast?:** Toast message or configuration to show on successful action.
- **updateServerAfter?:** Indicates when the server is eventually updated. Possible values:
  - `submit`: Server updates immediately after the submission promise resolves.
  - `number`: Server updates after a specified timeout (in milliseconds).
  - `Promise`: Server updates after a forwarded promise resolves. Default value is `submit`.
- **keepPosition?:** Indicates whether to keep the current position of items after the update.



### Props

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `createOne` | `(item: T, params: CreateManyParams<T>) => void` | Yes | - | Creates a single item in the collection. |
| `createMany` | `(items: T[], params: CreateManyParams<T>) => void` | Yes | - | Creates multiple items in the collection. |
| `updateMany` | `<R>(items: T[], params: UpdateManyParams<T, R>) => void` | Yes | - | Updates multiple items in the collection. @param items - The items to update. @param params - Additional parameters for the update operation. |
| `updateOne` | `<R>(item: T, params: UpdateManyParams<T, R>) => void` | Yes | - | Updates a single item in the collection. @param item - The item to update. @param params - Additional parameters for the operation. |
| `updateAll` | `{ <R>(patchOrFn: (item: T) => Partial<T>, params: UpdateAllParams<R>): void; <R>(patch: Partial<T>, params: UpdateAllParams<R>): void; }` | Yes | - | > `updateAll(fn)` API is not recommended as it's not possible to calculate "optimistic total", prefer updateAll(patchObject) API if possible. Updates all items in the collection based on a function or a patch object. Updates all items in the collection based on a patch object. @param patchOrFn - A function or a patch object to apply to each item. @param params - Additional parameters for the operation. @param patch - A patch object to apply to each item. @param params - Additional parameters for the operation. |
| `deleteMany` | `<R>(items: T[], params: DeleteManyParams<T, R>) => void` | Yes | - | Deletes multiple items from the collection. @param items - An array of items to delete. @param params - Additional parameters for the delete operation. |
| `deleteOne` | `<R>(item: T, params: DeleteManyParams<T, R>) => void` | Yes | - | Deletes a single item from the collection. @param item - The item to delete. @param params - Additional parameters for the operation. |
| `deleteAll` | `<R>(params: UpdateAllParams<R>) => void` | Yes | - | Deletes all items from the collection. @param params - Additional parameters for the operation. |

