# useOptimisticActions

**Category:** Features/Actions/Updates

## Design

### Description

The `useOptimisticActions` is a hook used to manage optimistic actions for a collection. It provides functionality to perform optimistic CRUD operations on a collection of items, such as creating, updating, and deleting items, while awaiting confirmation from the server. This hook is particularly useful in scenarios where immediate feedback to the user is desired, even before the server has processed the action.

The hook receives two parameters:
- **collection** - The collection state object representing the data collection to perform optimistic actions on.

- **params** - Optional parameters to configure the behavior of the optimistic actions.

The hook returns an instance of [CollectionOptimisticActions](./?path=/story/features-actions-updates--collectionoptimisticactions), which encapsulates the optimistic actions for the provided collection.

### Returns
[CollectionOptimisticActions](./?path=/story/features-actions-updates--collectionoptimisticactions)

### Usage
The following example uses the `useOptimisticActions` hook to initialize optimistic actions for the `state.collection`. It also passed optional parameters to configure the behavior of optimistic actions, such as custom predicate logic for filtering items.
```diff
- import { OffsetQuery, useTableCollection } from '@wix/patterns';
+ import { OffsetQuery, useTableCollection, useOptimisticActions } from '@wix/patterns';

  const state = useTableCollection({
    fetchData: (query: OffsetQuery) => {
      // server call to fetch data
    },
  });

+  const optimisticActions = useOptimisticActions(state.collection, {
+    orderBy: () => [],
+    predicate: ({ filters }) => {
+      // Custom predicate logic based on filters
+    },
+  });

  const promoteContacts = () => {
-    // show loading
-   const response = (await Promise.all(
-     contacts.map((contact) =>
-       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);

-    // update collection
-    // show result
+   optimisticActions.updateMany(updatedContacts, {
+     successToast: `${contacts.length} contacts updated`,
+     submit: async (contacts) => {
+       const response = (await Promise.all(
+         contacts.map((contact) =>
+           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);
+     },
+   });
+  };
```




```tsx
import { useOptimisticActions } from '@wix/patterns';
```

### Default behaviour

Default behaviour of create, update and delete.

```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 { Delete } from '@wix/wix-ui-icons-common';
import { contacts } from '@wix/crm';

function DefaultBehaviour() {
  const state = useTableCollection<contacts.Contact>({
    queryName: 'contacts-DefaultBehaviour',
    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);
      },
    });
  };

  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);
      },
    });
  };

  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}
          primaryActionButton={<PrimaryActionButton onClick={_createContact} />}
          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',
              onClick: () => {
                _updateContact(contact);
              },
            },
            secondaryActions: [
              {
                icon: <Delete />,
                text: 'Delete',
                onClick: () => {
                  _deleteContact(contact);
                },
              },
            ],
          })}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

### Success toast with undo

You have the option to postpone the server request, giving the user a few seconds to regret using "Undo" button. 
 You can also show a toast after the undo action with `showUndoSuccessToast`

```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 { Delete } from '@wix/wix-ui-icons-common';
import { contacts } from '@wix/crm';

function UndoBeforeSending() {
  const state = useTableCollection<contacts.Contact>({
    queryName: 'contacts-UndoBeforeSending',
    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, {
      showUndoToast: true,
      showUndoSuccessToast: true,
      successToast: 'New contact is created',
      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);
      },
    });
  };

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

    optimisticActions.updateOne(updatedContact, {
      showUndoToast: true,
      showUndoSuccessToast: true,
      successToast: 'A contact has been 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);
      },
    });
  };

  const _deleteContact = (contact: contacts.Contact) => {
    optimisticActions.deleteOne(contact, {
      showUndoToast: true,
      showUndoSuccessToast: true,
      successToast: 'A contact was deleted',
      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}
          primaryActionButton={<PrimaryActionButton onClick={_createContact} />}
          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',
              onClick: () => {
                _updateContact(contact);
              },
            },
            secondaryActions: [
              {
                icon: <Delete />,
                text: 'Delete',
                onClick: () => {
                  _deleteContact(contact);
                },
              },
            ],
          })}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

### Error handling and retries

In case of an error from the server, you can define what to show the user and what to do during it. 
 Additionally, you can allow the user the option to try to execute the action again

```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 { Delete } from '@wix/wix-ui-icons-common';
import { contacts } from '@wix/crm';

function ErrorHandlingAndRetries() {

  const state = useTableCollection<contacts.Contact>({
    queryName: 'contacts-ErrorHandlingAndRetries',
    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, {
      errorToast: (err) => 'failed to create a contact',
      onError: (err) => {
        // report error to server
        console.log('error');
      },
      onTryAgain: () => {
        // report error to server
        console.log('try again');
      },
      submit: async (contacts) => {
        return new Promise((resolve, reject) => {
          window.setTimeout(() => {
            reject(new Error('error'));
          }, 1000);
        });
      },
    });
  };

  const _updateContact = (contact: contacts.Contact) => {
    const updatedContact = {
      ...contact,
      info: {
        ...contact.info,
        jobTitle: 'Ultimate',
      },
    };
    optimisticActions.updateOne(updatedContact, {
      errorToast: (err) => 'failed to update a contact',
      onError: (err) => {
        // report error to server
        console.log('error');
      },
      onTryAgain: () => {
        // report error to server
        console.log('try again');
      },
      submit: async (contacts) => {
        return new Promise((resolve, reject) => {
          window.setTimeout(() => {
            reject(new Error('error'));
          }, 1000);
        });
      },
    });
  };

  const _deleteContact = (contact: contacts.Contact) => {
    optimisticActions.deleteOne(contact, {
      errorToast: (err) => 'failed to delete a contact',
      onError: (err) => {
        // report error to server
        console.log('error');
      },
      onTryAgain: () => {
        // report error to server
        console.log('try again');
      },
      submit: async (contacts) => {
        return new Promise((resolve, reject) => {
          window.setTimeout(() => {
            reject(new Error('error'));
          }, 1000);
        });
      },
    });
  };

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          state={collection}
          primaryActionButton={<PrimaryActionButton onClick={_createContact} />}
          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',
              onClick: () => {
                _updateContact(contact);
              },
            },
            secondaryActions: [
              {
                icon: <Delete />,
                text: 'Delete',
                onClick: () => {
                  _deleteContact(contact);
                },
              },
            ],
          })}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

### Items orders

You can decide the orders of the item with `OrdeyBy`

```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 { Delete } from '@wix/wix-ui-icons-common';
import { contacts } from '@wix/crm';

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

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

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

      queryBuilder = queryBuilder.ascending('info.name.first');

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

  const collection = state.collection;

  const optimisticActions = useOptimisticActions(collection, {
    orderBy: ({ sort }) => [
      ...(sort || []),
      {
        fieldName: (item) => item.info?.name?.first || '',
        order: 'asc',
      },
    ],
  });

  const _createContact = () => {
    const newContact = {
      id: new window.Date().toString(),
      info: {
        name: {
          first: 'Aaa',
          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);
      },
    });
  };

  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);
      },
    });
  };

  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}
          primaryActionButton={<PrimaryActionButton onClick={_createContact} />}
          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',
              onClick: () => {
                _updateContact(contact);
              },
            },
            secondaryActions: [
              {
                icon: <Delete />,
                text: 'Delete',
                onClick: () => {
                  _deleteContact(contact);
                },
              },
            ],
          })}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

### Update with query

When you update the data when query exists (filters, search) you need to define `predicate` and `orderBy` in order to update the relevant items.
 In this example, try to update the filtered items, remove the filters and see that only the filtered items were updated.

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  BulkActionButton,
  BulkActionToolbar,
  CollectionToolbarFilters,
  Filter,
  Table,
  MultiSelectCheckboxFilter,
  OffsetQuery,
  stringsArrayFilter,
  useTableCollection,
  useOptimisticActions,
  useStaticListFilterCollection,
} 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';

type ContactsFilters = {
  level: Filter<string[]>;
};

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

  const state = useTableCollection<Contact, ContactsFilters>({
    queryName: 'contacts-OrderBy',
    fetchData: (query: OffsetQuery<ContactsFilters>) => {
      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',
    filters: {
      level: stringsArrayFilter({
        initialValue: ['Beginner'],
      }),
    },
  });

  const collection = state.collection;

  const optimisticActions = useOptimisticActions(collection, {
    predicate: ({ filters }) => {
      const { level } = filters;
      return (item) => {
        if (level && !level.some((level) => level === String(item.level))) {
          return false;
        }
        return true;
      };
    },
    orderBy: ({ sort }) => [
      ...(sort || []),
      {
        fieldName: (item) => item.createdDate || new Date(0),
        order: 'desc',
      },
    ],
  });

  const _updateContacts = (
    contacts: Contact[],
    { allSelected, total }: { allSelected: boolean; total: number },
  ) => {
    if (allSelected) {
      const newNameContact = { name: 'new name' } as Partial<Contact>;

      optimisticActions.updateAll(newNameContact, {
        successToast: `${total} contacts changed`,
        submit: () =>
          httpClient.request(
            bulkUpdateContacts({
              filter: {
                level: collection.filters.level.value,
              },
              info: newNameContact as ContactInfo,
            }),
          ),
      });
    } else {
      const updatedContacts = contacts.map((c) => ({ ...c, name: 'new name' }));

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

  const levelsCollection = useStaticListFilterCollection(
    collection.filters.level,
    [
      'Beginner',
      'Amateur',
      'Semi-Pro',
      'Professional',
      'World Class',
      'Legendary',
      'Ultimate',
    ],
  );

  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(),
            },
          ]}
          filters={
            <CollectionToolbarFilters>
              <MultiSelectCheckboxFilter
                accordionItemProps={{ label: 'Level' }}
                popoverProps={{ appendTo: 'window' }}
                filter={collection.filters.level!}
                collection={levelsCollection}
                renderItem={(level) => ({ title: level })}
              />
            </CollectionToolbarFilters>
          }
          showSelection
          bulkActionToolbar={({
            selectedValues,
            clearSelection,
            allSelected,
            total,
          }) => (
            <BulkActionToolbar
              actionButton={
                <BulkActionButton
                  onClick={() => {
                    _updateContacts(selectedValues, { allSelected, total });
                    clearSelection();
                  }}
                >
                  Update Contacts' Names
                </BulkActionButton>
              }
            />
          )}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

### Revision handling

In case your server works with revision, you can need to update the revision by yourself

```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 RevisionHandling() {
  const state = useTableCollection<contacts.Contact>({
    queryName: 'contacts-RevisionHandling',
    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',
      },
      revision: +contact.revision! + 1,
    } as Partial<contacts.Contact>;

    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',
              onClick: () => {
                _updateContact(contact);
              },
            },
          })}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

### Custom error handling

You can define your own error handling wirh errorToast prop

```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 { Delete } from '@wix/wix-ui-icons-common';
import { contacts } from '@wix/crm';

function CustomErrorHandling() {

  const state = useTableCollection<contacts.Contact>({
    queryName: 'contacts-CustomErrorHandling',
    itemName: (item) => `${item.info?.name?.first} ${item.info?.name?.last}`,
    fetchData: (query: OffsetQuery) => {
      const { limit, offset, search } = 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, {
      errorToast: (err, { retry }) => ({
        message: 'failed to create a contact',
        action: {
          text: 'My custom action',
          onClick: () => {
            retry();
            console.log('retry');
          },
        },
      }),
      submit: async (contacts) => {
        return new Promise((resolve, reject) => {
          window.setTimeout(() => {
            reject(new Error('error'));
          }, 1000);
        });
      },
    });
  };

  const _updateContact = (contact: contacts.Contact) => {
    const updatedContact = {
      ...contact,
      info: {
        ...contact.info,
        jobTitle: 'Ultimate',
      },
    };
    optimisticActions.updateOne(updatedContact, {
      errorToast: (err, { retry }) => ({
        message: 'failed to update a contact',
        action: {
          text: 'My custom action',
          onClick: () => {
            retry();
            console.log('retry');
          },
        },
      }),
      submit: async (contacts) => {
        return new Promise((resolve, reject) => {
          window.setTimeout(() => {
            reject(new Error('error'));
          }, 1000);
        });
      },
    });
  };

  const _deleteContact = (contact: contacts.Contact) => {
    optimisticActions.deleteOne(contact, {
      errorToast: (err, { retry }) => ({
        message: 'failed to delete a contact',
        action: {
          text: 'My custom action',
          onClick: () => {
            retry();
            console.log('retry');
          },
        },
      }),
      submit: async (contacts) => {
        return new Promise((resolve, reject) => {
          window.setTimeout(() => {
            reject(new Error('error'));
          }, 1000);
        });
      },
    });
  };

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          state={collection}
          primaryActionButton={<PrimaryActionButton onClick={_createContact} />}
          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',
              onClick: () => {
                _updateContact(contact);
              },
            },
            secondaryActions: [
              {
                icon: <Delete />,
                text: 'Delete',
                onClick: () => {
                  _deleteContact(contact);
                },
              },
            ],
          })}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

## API

### Props

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `collection` | `CollectionState<T, F>` | No | - | The collection state object representing the data collection to perform optimistic actions on |
| `predicate` | `((computedQuery: ComputedQuery<F>) => (item: T) => boolean)` | No | - | A function that simulates the filtering in the server. The function should return another function that accepts an item, and return a boolean that indicates whether the item passes the query filters or not. @param computedQuery - The computed query object. |
| `orderBy` | `((computedQuery: ComputedQuery<F>) => Order<T>[])` | No | - | A function that simulates the ordering of the items in the server.<br> The function should return a list of rules in the form of `{fieldName: string; order: 'asc' \| 'desc';}` to order the items by @param computedQuery - The computed query object. |
| `createdAt` | `((item: T) => Date)` | No | - | A function to retrieve the creation date of an item. @param item - The item. |
| `batchWait` | `number` | No | `0` | Time to wait before batching multiple `updateMany` calls into a single server call. |
| `noInvalidate` | `boolean` | No | - | Indicates whether to refresh the current page without invalidating the cache. |
| `cacheNamespace` | `string` | No | - | Adds a namespace to the cache key. |

