---
title: "Optimistic UI"
section: "frontend/data"
platforms: ["angular", "javascript", "nextjs", "react", "swift", "vue"]
gen: 2
last-updated: "2026-03-25T17:40:00.000Z"
url: "https://docs.amplify.aws/react/frontend/data/optimistic-ui/"
---

<!-- Platform: javascript, angular, nextjs, vue, react -->
Amplify Data can be used with [TanStack Query](https://tanstack.com/query/latest/docs/react/overview) to implement optimistic UI, allowing CRUD operations to be rendered immediately on the UI before the request roundtrip has completed. Using Amplify Data with TanStack additionally makes it easy to render loading and error states, and allows you to rollback changes on the UI when API calls are unsuccessful.

In the following examples we'll create a list view that optimistically renders newly created items, and a detail view that optimistically renders updates and deletes.

<Callout>

For more details on TanStack Query, including requirements, supported browsers, and advanced usage, see the [TanStack Query documentation](https://tanstack.com/query/latest/docs/react/overview).
For complete guidance on how to implement optimistic updates with TanStack Query, see the [TanStack Query Optimistic UI Documentation](https://tanstack.com/query/latest/docs/react/guides/optimistic-updates).
For more on Amplify Data, see the [API documentation](/[platform]/build-a-backend/data/set-up-data/).

</Callout>

To get started, run the following command in an existing Amplify project with a React frontend:

```bash title="Terminal" showLineNumbers={false}
npm add @tanstack/react-query && \
npm add --save-dev @tanstack/react-query-devtools
```

Modify your Data schema to use this "Real Estate Property" example:

```ts title="amplify/data/resource.ts"
const schema = a.schema({
  RealEstateProperty: a.model({
    name: a.string().required(),
    address: a.string(),
  }).authorization(allow => [allow.guest()])
})

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'iam',
  },
});
```

Save the file and run `npx ampx sandbox` to deploy the changes to your backend cloud sandbox. For the purposes of this guide, we'll build a Real Estate Property listing application.

Next, at the root of your project, add the required TanStack Query imports, and create a client:

```ts title="src/main.tsx"
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import { Amplify } from 'aws-amplify'
import outputs from '../amplify_outputs.json'
// highlight-start
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
// highlight-end

Amplify.configure(outputs)

const queryClient = new QueryClient()

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    // highlight-start
    <QueryClientProvider client={queryClient}>
      <App />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
    // highlight-end
  </React.StrictMode>,
)
```

<Callout>

TanStack Query Devtools are not required, but are a useful resource for debugging and understanding how TanStack works under the hood. By default, React Query Devtools are only included in bundles when `process.env.NODE_ENV === 'development'`, meaning that no additional configuration is required to exclude them from a production build.
For more information on the TanStack Query Devtools, visit the [TanStack Query Devtools docs](https://tanstack.com/query/latest/docs/react/devtools)

</Callout>

<Callout>

For the complete working example, including required imports and React component state management, see the [Complete Example](#complete-example) below.

</Callout>

## How to use TanStack Query query keys with the Amplify Data API

TanStack Query manages query caching based on the query keys you specify. A query key must be an array. The array can contain a single string or multiple strings and nested objects. The query key must be serializable, and unique to the query's data.

When using TanStack to render optimistic UI with Amplify Data, you must use different query keys depending on the API operation. When retrieving a list of items, a single string is used (e.g. `queryKey: ["realEstateProperties"]`). This query key is also used to optimistically render a newly created item. When updating or deleting an item, the query key must also include the unique identifier for the record being deleted or updated (e.g. `queryKey: ["realEstateProperties", newRealEstateProperty.id]`).

For more detailed information on query keys, see the [TanStack Query documentation](https://tanstack.com/query/v4/docs/react/guides/query-keys).

## Optimistically rendering a list of records

To optimistically render a list of items returned from the Amplify Data API, use the TanStack `useQuery` hook, passing in the Data API query as the `queryFn` parameter. The following example creates a query to retrieve all records from the API. We'll use `realEstateProperties` as the query key, which will be the same key we use to optimistically render a newly created item.

```ts title="src/App.tsx"
// highlight-start
import type { Schema } from '../amplify/data/resource'
import { generateClient } from 'aws-amplify/data'
import { useQuery } from '@tanstack/react-query'

const client = generateClient<Schema>();
// highlight-end

function App() {
  // highlight-start
  const {
    data: realEstateProperties,
    isLoading,
    isSuccess,
    isError: isErrorQuery,
  } = useQuery({
    queryKey: ["realEstateProperties"],
    queryFn: async () => {
      const response = await client.models.RealEstateProperty.list();

      const allRealEstateProperties = response.data;

      if (!allRealEstateProperties) return null;

      return allRealEstateProperties;
    },
  });
  // highlight-end
  // return ...
}
```

## Optimistically rendering a newly created record

To optimistically render a newly created record returned from the Amplify Data API, use the TanStack `useMutation` hook, passing in the Amplify Data API mutation as the `mutationFn` parameter. We'll use the same query key used by the `useQuery` hook (`realEstateProperties`) as the query key to optimistically render a newly created item.
We'll use the `onMutate` function to update the cache directly, as well as the `onError` function to rollback changes when a request fails.

```ts
import { generateClient } from 'aws-amplify/api'
import type { Schema } from '../amplify/data/resource'
// highlight-next-line
import { useQueryClient, useMutation } from '@tanstack/react-query'

const client = generateClient<Schema>()

function App() {
  // highlight-next-line
  const queryClient = useQueryClient();

  // highlight-start
  const createMutation = useMutation({
    mutationFn: async (input: { name: string, address: string }) => {
      const { data: newRealEstateProperty } = await client.models.RealEstateProperty.create(input)
      return newRealEstateProperty;
    },
    // When mutate is called:
    onMutate: async (newRealEstateProperty) => {
      // Cancel any outgoing refetches
      // (so they don't overwrite our optimistic update)
      await queryClient.cancelQueries({ queryKey: ["realEstateProperties"] });

      // Snapshot the previous value
      const previousRealEstateProperties = queryClient.getQueryData([
        "realEstateProperties",
      ]);

      // Optimistically update to the new value
      if (previousRealEstateProperties) {
        queryClient.setQueryData(["realEstateProperties"], (old: Schema["RealEstateProperty"]["type"][]) => [
          ...old,
          newRealEstateProperty,
        ]);
      }

      // Return a context object with the snapshotted value
      return { previousRealEstateProperties };
    },
    // If the mutation fails,
    // use the context returned from onMutate to rollback
    onError: (err, newRealEstateProperty, context) => {
      console.error("Error saving record:", err, newRealEstateProperty);
      if (context?.previousRealEstateProperties) {
        queryClient.setQueryData(
          ["realEstateProperties"],
          context.previousRealEstateProperties
        );
      }
    },
    // Always refetch after error or success:
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ["realEstateProperties"] });
    },
  });
  // highlight-end
  // return ...
}
```

## Querying a single item with TanStack Query

To optimistically render updates on a single item, we'll first retrieve the item from the API. We'll use the `useQuery` hook, passing in the `get` query as the `queryFn` parameter. For the query key, we'll use a combination of `realEstateProperties` and the record's unique identifier.

```ts
import { generateClient } from 'aws-amplify/data'
import type { Schema } from '../amplify/data/resource'
import { useQuery } from '@tanstack/react-query'

const client = generateClient<Schema>()

function App() {
  const currentRealEstatePropertyId = "SOME_ID"
  // highlight-start
  const {
    data: realEstateProperty,
    isLoading,
    isSuccess,
    isError: isErrorQuery,
  } = useQuery({
    queryKey: ["realEstateProperties", currentRealEstatePropertyId],
    queryFn: async () => {
      if (!currentRealEstatePropertyId) { return }

      const { data: property } = await client.models.RealEstateProperty.get({
        id: currentRealEstatePropertyId,
      });
      return property;
    },
  });
  // highlight-end
}
```

## Optimistically render updates for a record

To optimistically render Amplify Data updates for a single record, use the TanStack `useMutation` hook, passing in the update mutation as the `mutationFn` parameter. We'll use the same query key combination used by the single record `useQuery` hook (`realEstateProperties` and the record's `id`) as the query key to optimistically render the updates.
We'll use the `onMutate` function to update the cache directly, as well as the `onError` function to rollback changes when a request fails.

<Callout>

When directly interacting with the cache via the `onMutate` function, the `newRealEstateProperty` parameter will only include fields that are being updated. When calling `setQueryData`, include the previous values for all fields in addition to the newly updated fields to avoid only rendering optimistic values for updated fields on the UI.

</Callout>

```ts title="src/App.tsx"
import { generateClient } from 'aws-amplify/data'
import type { Schema } from '../amplify/data/resource'
import { useQueryClient, useMutation } from "@tanstack/react-query";

const client = generateClient<Schema>()

function App() {
  // highlight-next-line
  const queryClient = useQueryClient();

  // highlight-start
   const updateMutation = useMutation({
    mutationFn: async (realEstatePropertyDetails: { id: string, name?: string, address?: string }) => {
      const { data: updatedProperty } = await client.models.RealEstateProperty.update(realEstatePropertyDetails);

      return updatedProperty;
    },
    // When mutate is called:
    onMutate: async (newRealEstateProperty: { id: string, name?: string, address?: string }) => {
      // Cancel any outgoing refetches
      // (so they don't overwrite our optimistic update)
      await queryClient.cancelQueries({
        queryKey: ["realEstateProperties", newRealEstateProperty.id],
      });

      await queryClient.cancelQueries({
        queryKey: ["realEstateProperties"],
      });

      // Snapshot the previous value
      const previousRealEstateProperty = queryClient.getQueryData([
        "realEstateProperties",
        newRealEstateProperty.id,
      ]);

      // Optimistically update to the new value
      if (previousRealEstateProperty) {
        queryClient.setQueryData(
          ["realEstateProperties", newRealEstateProperty.id],
          /**
           * `newRealEstateProperty` will at first only include updated values for
           * the record. To avoid only rendering optimistic values for updated
           * fields on the UI, include the previous values for all fields:
           */
          { ...previousRealEstateProperty, ...newRealEstateProperty }
        );
      }

      // Return a context with the previous and new realEstateProperty
      return { previousRealEstateProperty, newRealEstateProperty };
    },
    // If the mutation fails, use the context we returned above
    onError: (err, newRealEstateProperty, context) => {
      console.error("Error updating record:", err, newRealEstateProperty);
      if (context?.previousRealEstateProperty) {
        queryClient.setQueryData(
          ["realEstateProperties", context.newRealEstateProperty.id],
          context.previousRealEstateProperty
        );
      }
    },
    // Always refetch after error or success:
    onSettled: (newRealEstateProperty) => {
      if (newRealEstateProperty) {
        queryClient.invalidateQueries({
          queryKey: ["realEstateProperties", newRealEstateProperty.id],
        });
        queryClient.invalidateQueries({
          queryKey: ["realEstateProperties"],
        });
      }
    },
  });
  // highlight-end
}
```

## Optimistically render deleting a record

To optimistically render a deletion of a single record, use the TanStack `useMutation` hook, passing in the delete mutation as the `mutationFn` parameter. We'll use the same query key combination used by the single record `useQuery` hook (`realEstateProperties` and the record's `id`) as the query key to optimistically render the updates.
We'll use the `onMutate` function to update the cache directly, as well as the `onError` function to rollback changes when a delete fails.

```ts title="src/App.tsx"
import { generateClient } from 'aws-amplify/data'
import type { Schema } from '../amplify/data/resource'
import { useQueryClient, useMutation } from '@tanstack/react-query'

const client = generateClient<Schema>()

function App() {
  // highlight-next-line
  const queryClient = useQueryClient();

  // highlight-start
    const deleteMutation = useMutation({
    mutationFn: async (realEstatePropertyDetails: { id: string }) => {
      const { data: deletedProperty } = await client.models.RealEstateProperty.delete(realEstatePropertyDetails);
      return deletedProperty;
    },
    // When mutate is called:
    onMutate: async (newRealEstateProperty) => {
      // Cancel any outgoing refetches
      // (so they don't overwrite our optimistic update)
      await queryClient.cancelQueries({
        queryKey: ["realEstateProperties", newRealEstateProperty.id],
      });

      await queryClient.cancelQueries({
        queryKey: ["realEstateProperties"],
      });

      // Snapshot the previous value
      const previousRealEstateProperty = queryClient.getQueryData([
        "realEstateProperties",
        newRealEstateProperty.id,
      ]);

      // Optimistically update to the new value
      if (previousRealEstateProperty) {
        queryClient.setQueryData(
          ["realEstateProperties", newRealEstateProperty.id],
          newRealEstateProperty
        );
      }

      // Return a context with the previous and new realEstateProperty
      return { previousRealEstateProperty, newRealEstateProperty };
    },
    // If the mutation fails, use the context we returned above
    onError: (err, newRealEstateProperty, context) => {
      console.error("Error deleting record:", err, newRealEstateProperty);
      if (context?.previousRealEstateProperty) {
        queryClient.setQueryData(
          ["realEstateProperties", context.newRealEstateProperty.id],
          context.previousRealEstateProperty
        );
      }
    },
    // Always refetch after error or success:
    onSettled: (newRealEstateProperty) => {
      if (newRealEstateProperty) {
        queryClient.invalidateQueries({
          queryKey: ["realEstateProperties", newRealEstateProperty.id],
        });
        queryClient.invalidateQueries({
          queryKey: ["realEstateProperties"],
        });
      }
    },
  });
  // highlight-end
}
```

## Loading and error states for optimistically rendered data

Both `useQuery` and `useMutation` return `isLoading` and `isError` states that indicate the current state of the query or mutation. You can use these states to render loading and error indicators.

In addition to operation-specific loading states, TanStack Query provides a [`useIsFetching` hook](https://www.tanstack.com/query/v4/docs/react/guides/background-fetching-indicators#displaying-global-background-fetching-loading-state). For the purposes of this demo, we show a global loading indicator in the [Complete Example](#complete-example) when *any* queries are fetching (including in the background) in order to help visualize what TanStack is doing in the background:

```js
function GlobalLoadingIndicator() {
  const isFetching = useIsFetching();
  return isFetching ? <div style={styles.globalLoadingIndicator}></div> : null;
}
```

For more details on advanced usage of TanStack Query hooks, see the [TanStack documentation](https://tanstack.com/query/latest/docs/react/guides/mutations).

The following example demonstrates how to use the state returned by TanStack to render a loading indicator while a mutation is in progress, and an error message if the mutation fails. For additional examples, see the [Complete Example](#complete-example) below.

```ts
<>
  {updateMutation.isError &&
  updateMutation.error instanceof Error ? (
    <div>An error occurred: {updateMutation.error.message}</div>
  ) : null}

  {updateMutation.isSuccess ? (
    <div>Real Estate Property updated!</div>
  ) : null}

  <button
    onClick={() =>
      updateMutation.mutate({
        id: realEstateProperty.id,
        address: `${Math.floor(
          1000 + Math.random() * 9000
        )} Main St`,
      })
    }
  >
    Update Address
  </button>
</>
```

## Complete example

```tsx title="src/main.tsx"
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import { Amplify } from 'aws-amplify'
import outputs from '../amplify_outputs.json'
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

Amplify.configure(outputs)

export const queryClient = new QueryClient()

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  </React.StrictMode>,
)
```

```tsx title="src/App.tsx"
import { generateClient } from 'aws-amplify/data'
import type { Schema } from '../amplify/data/resource'
import './App.css'
import { useIsFetching, useMutation, useQuery } from '@tanstack/react-query'
import { queryClient } from './main'
import { useState } from 'react'

const client = generateClient<Schema>({
  authMode: 'iam'
})

function GlobalLoadingIndicator() {
  const isFetching = useIsFetching();

  return isFetching ? <div style={styles.globalLoadingIndicator}></div> : null;
}

function App() {
  const [currentRealEstatePropertyId, setCurrentRealEstatePropertyId] =
  useState<string | null>(null);

  const {
    data: realEstateProperties,
    isLoading,
    isSuccess,
    isError: isErrorQuery,
  } = useQuery({
    queryKey: ["realEstateProperties"],
    queryFn: async () => {
      const response = await client.models.RealEstateProperty.list();

      const allRealEstateProperties = response.data;

      if (!allRealEstateProperties) return null;

      return allRealEstateProperties;
    },
  });

  const createMutation = useMutation({
    mutationFn: async (input: { name: string, address: string }) => {
      const { data: newRealEstateProperty } = await client.models.RealEstateProperty.create(input)
      return newRealEstateProperty;
    },
    // When mutate is called:
    onMutate: async (newRealEstateProperty) => {
      // Cancel any outgoing refetches
      // (so they don't overwrite our optimistic update)
      await queryClient.cancelQueries({ queryKey: ["realEstateProperties"] });

      // Snapshot the previous value
      const previousRealEstateProperties = queryClient.getQueryData([
        "realEstateProperties",
      ]);

      // Optimistically update to the new value
      if (previousRealEstateProperties) {
        queryClient.setQueryData(["realEstateProperties"], (old: Schema["RealEstateProperty"]["type"][]) => [
          ...old,
          newRealEstateProperty,
        ]);
      }

      // Return a context object with the snapshotted value
      return { previousRealEstateProperties };
    },
    // If the mutation fails,
    // use the context returned from onMutate to rollback
    onError: (err, newRealEstateProperty, context) => {
      console.error("Error saving record:", err, newRealEstateProperty);
      if (context?.previousRealEstateProperties) {
        queryClient.setQueryData(
          ["realEstateProperties"],
          context.previousRealEstateProperties
        );
      }
    },
    // Always refetch after error or success:
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ["realEstateProperties"] });
    },
  });

  function RealEstatePropertyDetailView() {

    const {
      data: realEstateProperty,
      isLoading,
      isSuccess,
      isError: isErrorQuery,
    } = useQuery({
      queryKey: ["realEstateProperties", currentRealEstatePropertyId],
      queryFn: async () => {
        if (!currentRealEstatePropertyId) { return }

        const { data: property } = await client.models.RealEstateProperty.get({ id: currentRealEstatePropertyId });
        return property
      },
    });

    const updateMutation = useMutation({
      mutationFn: async (realEstatePropertyDetails: { id: string, name?: string, address?: string }) => {
        const { data: updatedProperty } = await client.models.RealEstateProperty.update(realEstatePropertyDetails);

        return updatedProperty;
      },
      // When mutate is called:
      onMutate: async (newRealEstateProperty: { id: string, name?: string, address?: string }) => {
        // Cancel any outgoing refetches
        // (so they don't overwrite our optimistic update)
        await queryClient.cancelQueries({
          queryKey: ["realEstateProperties", newRealEstateProperty.id],
        });

        await queryClient.cancelQueries({
          queryKey: ["realEstateProperties"],
        });

        // Snapshot the previous value
        const previousRealEstateProperty = queryClient.getQueryData([
          "realEstateProperties",
          newRealEstateProperty.id,
        ]);

        // Optimistically update to the new value
        if (previousRealEstateProperty) {
          queryClient.setQueryData(
            ["realEstateProperties", newRealEstateProperty.id],
            /**
             * `newRealEstateProperty` will at first only include updated values for
             * the record. To avoid only rendering optimistic values for updated
             * fields on the UI, include the previous values for all fields:
             */
            { ...previousRealEstateProperty, ...newRealEstateProperty }
          );
        }

        // Return a context with the previous and new realEstateProperty
        return { previousRealEstateProperty, newRealEstateProperty };
      },
      // If the mutation fails, use the context we returned above
      onError: (err, newRealEstateProperty, context) => {
        console.error("Error updating record:", err, newRealEstateProperty);
        if (context?.previousRealEstateProperty) {
          queryClient.setQueryData(
            ["realEstateProperties", context.newRealEstateProperty.id],
            context.previousRealEstateProperty
          );
        }
      },
      // Always refetch after error or success:
      onSettled: (newRealEstateProperty) => {
        if (newRealEstateProperty) {
          queryClient.invalidateQueries({
            queryKey: ["realEstateProperties", newRealEstateProperty.id],
          });
          queryClient.invalidateQueries({
            queryKey: ["realEstateProperties"],
          });
        }
      },
    });

    const deleteMutation = useMutation({
      mutationFn: async (realEstatePropertyDetails: { id: string }) => {
        const { data: deletedProperty } = await client.models.RealEstateProperty.delete(realEstatePropertyDetails);
        return deletedProperty;
      },
      // When mutate is called:
      onMutate: async (newRealEstateProperty) => {
        // Cancel any outgoing refetches
        // (so they don't overwrite our optimistic update)
        await queryClient.cancelQueries({
          queryKey: ["realEstateProperties", newRealEstateProperty.id],
        });

        await queryClient.cancelQueries({
          queryKey: ["realEstateProperties"],
        });

        // Snapshot the previous value
        const previousRealEstateProperty = queryClient.getQueryData([
          "realEstateProperties",
          newRealEstateProperty.id,
        ]);

        // Optimistically update to the new value
        if (previousRealEstateProperty) {
          queryClient.setQueryData(
            ["realEstateProperties", newRealEstateProperty.id],
            newRealEstateProperty
          );
        }

        // Return a context with the previous and new realEstateProperty
        return { previousRealEstateProperty, newRealEstateProperty };
      },
      // If the mutation fails, use the context we returned above
      onError: (err, newRealEstateProperty, context) => {
        console.error("Error deleting record:", err, newRealEstateProperty);
        if (context?.previousRealEstateProperty) {
          queryClient.setQueryData(
            ["realEstateProperties", context.newRealEstateProperty.id],
            context.previousRealEstateProperty
          );
        }
      },
      // Always refetch after error or success:
      onSettled: (newRealEstateProperty) => {
        if (newRealEstateProperty) {
          queryClient.invalidateQueries({
            queryKey: ["realEstateProperties", newRealEstateProperty.id],
          });
          queryClient.invalidateQueries({
            queryKey: ["realEstateProperties"],
          });
        }
      },
    });

    return (
      <div style={styles.detailViewContainer}>
        <h2>Real Estate Property Detail View</h2>
        {isErrorQuery && <div>{"Problem loading Real Estate Property"}</div>}
        {isLoading && (
          <div style={styles.loadingIndicator}>
            {"Loading Real Estate Property..."}
          </div>
        )}
        {isSuccess && (
          <div>
            <p>{`Name: ${realEstateProperty?.name}`}</p>
            <p>{`Address: ${realEstateProperty?.address}`}</p>
          </div>
        )}
        {realEstateProperty && (
          <div>
            <div>
              {updateMutation.isPending ? (
                "Updating Real Estate Property..."
              ) : (
                <>
                  {updateMutation.isError &&
                    updateMutation.error instanceof Error ? (
                    <div>An error occurred: {updateMutation.error.message}</div>
                  ) : null}

                  {updateMutation.isSuccess ? (
                    <div>Real Estate Property updated!</div>
                  ) : null}

                  <button
                    onClick={() =>
                      updateMutation.mutate({
                        id: realEstateProperty.id,
                        name: `Updated Home ${Date.now()}`,
                      })
                    }
                  >
                    Update Name
                  </button>
                  <button
                    onClick={() =>
                      updateMutation.mutate({
                        id: realEstateProperty.id,
                        address: `${Math.floor(
                          1000 + Math.random() * 9000
                        )} Main St`,
                      })
                    }
                  >
                    Update Address
                  </button>
                </>
              )}
            </div>

            <div>
              {deleteMutation.isPending ? (
                "Deleting Real Estate Property..."
              ) : (
                <>
                  {deleteMutation.isError &&
                    deleteMutation.error instanceof Error ? (
                    <div>An error occurred: {deleteMutation.error.message}</div>
                  ) : null}

                  {deleteMutation.isSuccess ? (
                    <div>Real Estate Property deleted!</div>
                  ) : null}

                  <button
                    onClick={() =>
                      deleteMutation.mutate({
                        id: realEstateProperty.id,
                      })
                    }
                  >
                    Delete
                  </button>
                </>
              )}
            </div>
          </div>
        )}
        <button onClick={() => setCurrentRealEstatePropertyId(null)}>
          Back
        </button>
      </div>
    );

  }
  return (
    <div>
      {!currentRealEstatePropertyId && (
        <div style={styles.appContainer}>
          <h1>Real Estate Properties:</h1>
          <div>
            {createMutation.isPending ? (
              "Adding Real Estate Property..."
            ) : (
              <>
                {createMutation.isError &&
                createMutation.error instanceof Error ? (
                  <div>An error occurred: {createMutation.error.message}</div>
                ) : null}

                {createMutation.isSuccess ? (
                  <div>Real Estate Property added!</div>
                ) : null}

                <button
                  onClick={() => {
                    createMutation.mutate({
                      name: `New Home ${Date.now()}`,
                      address: `${Math.floor(
                        1000 + Math.random() * 9000
                      )} Main St`,
                    });
                  }}
                >
                  Add RealEstateProperty
                </button>
              </>
            )}
          </div>
          <ul style={styles.propertiesList}>
            {isLoading && (
              <div style={styles.loadingIndicator}>
                {"Loading Real Estate Properties..."}
              </div>
            )}
            {isErrorQuery && (
              <div>{"Problem loading Real Estate Properties"}</div>
            )}
            {isSuccess &&
              realEstateProperties?.map((realEstateProperty, idx) => {
                if (!realEstateProperty) return null;
                return (
                  <li
                    style={styles.listItem}
                    key={`${idx}-${realEstateProperty.id}`}
                  >
                    <p>{realEstateProperty.name}</p>
                    <button
                      style={styles.detailViewButton}
                      onClick={() =>
                        setCurrentRealEstatePropertyId(realEstateProperty.id)
                      }
                    >
                      Detail View
                    </button>
                  </li>
                );
              })}
          </ul>
        </div>
      )}
      {currentRealEstatePropertyId && <RealEstatePropertyDetailView />}
      <GlobalLoadingIndicator />
    </div>
  );

}

export default App

const styles = {
  appContainer: {
    display: "flex",
    flexDirection: "column",
    alignItems: "center",
  },
  detailViewButton: { marginLeft: "1rem" },
  detailViewContainer: { border: "1px solid black", padding: "3rem" },
  globalLoadingIndicator: {
    position: "fixed",
    top: 0,
    left: 0,
    width: "100%",
    height: "100%",
    border: "4px solid blue",
    pointerEvents: "none",
  },
  listItem: {
    display: "flex",
    justifyContent: "space-between",
    border: "1px dotted grey",
    padding: ".5rem",
    margin: ".1rem",
  },
  loadingIndicator: {
    border: "1px solid black",
    padding: "1rem",
    margin: "1rem",
  },
  propertiesList: {
    display: "flex",
    flexDirection: "column",
    alignItems: "center",
    justifyContent: "start",
    width: "50%",
    border: "1px solid black",
    padding: "1rem",
    listStyleType: "none",
  },
} as const;
```
<!-- /Platform -->

<!-- Platform: swift -->
Implementing optimistic UI with Amplify Data allows CRUD operations to be rendered immediately on the UI before the request roundtrip has completed, and allows you to rollback changes on the UI when API calls are unsuccessful.

In the following example, we'll create a list view that optimistically renders newly created items, updates and deletes.  Modify your Data schema to use this "Real Estate Property" example:

```ts title="amplify/data/resource.ts"
const schema = a.schema({
  RealEstateProperty: a.model({
    name: a.string().required(),
    address: a.string(),
  }).authorization(allow => [allow.guest()])
})

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'iam',
  },
});
```

Save the file and run `npx ampx sandbox` to deploy the changes to your backend cloud sandbox. For the purposes of this guide, we'll build a Real Estate Property listing application.

Once the backend has been provisioned, run `npx ampx generate graphql-client-code --format modelgen --model-target swift --out <path_to_swift_project>/AmplifyModels` to generate the Swift model types for the app.

Next, add the Amplify(`https://github.com/aws-amplify/amplify-swift.git`) package to your Xcode project and select the following modules to import when prompted:

- AWSAPIPlugin
- AWSCognitoAuthPlugin
- AWSS3StoragePlugin
- Amplify

<Callout>

For the complete working example see the [Complete Example](#complete-example) below.

</Callout>

## How to use a Swift Actor to perform optimistic UI updates

A Swift actor serializes access to its underlying properties. In this example, the actor will hold a list of items that will be published to the UI through a Combine publisher whenever the list is accessed. On a high level, the methods on the actor will perform the following:

- create a new model, add it to the list, remove the newly added item from the list if the API request is unsuccessful
- update the existing model in the list, revert the update on the list if the API request is unsuccessful
- delete the existing model from the list, add the item back into the list if the API request is unsuccessful

By providing these methods through an actor object, the underlying list will be accessed serially so that the entire operation can be rolled back if needed.

To create an actor object that allows optimistic UI updates, create a new file and add the following code.

```swift
import Amplify
import SwiftUI
import Combine

actor RealEstatePropertyList {

    private var properties: [RealEstateProperty?] = [] {
        didSet {
            subject.send(properties.compactMap { $0 })
        }
    }

    private let subject = PassthroughSubject<[RealEstateProperty], Never>()
    var publisher: AnyPublisher<[RealEstateProperty], Never> {
        subject.eraseToAnyPublisher()
    }

    func listProperties() async throws {
        let result = try await Amplify.API.query(request: .list(RealEstateProperty.self))
        guard case .success(let propertyList) = result else {
            print("Failed with error: ", result)
            return
        }
        properties = propertyList.elements
    }
}
```

Calling the `listProperties()` method will perform a query with Amplify Data API and store the results in the `properties` property. When this property is set, the list is sent back to the subscribers. In your UI, create a view model and subscribe to updates:

```swift
class RealEstatePropertyContainerViewModel: ObservableObject {
    @Published var properties: [RealEstateProperty] = []
    var sink: AnyCancellable?

    var propertyList = RealEstatePropertyList()
    init() {
        Task {
            sink = await propertyList.publisher
                .receive(on: DispatchQueue.main)
                .sink { properties in
                    print("Updating property list")
                    self.properties = properties
            }
        }
    }

    func loadList() {
        Task {
            try? await propertyList.listProperties()
        }
    }
}

struct RealEstatePropertyContainerView: View {
    @StateObject var vm = RealEstatePropertyContainerViewModel()
    @State private var propertyName: String = ""

    var body: some View {
        Text("Hello")
    }
}
```

## Optimistically rendering a newly created record

To optimistically render a newly created record returned from the Amplify Data API, add a method to the `actor RealEstatePropertyList`:

```swift
func createProperty(name: String, address: String? = nil) {
    let property = RealEstateProperty(name: name, address: address)
    // Optimistically send the newly created property, for the UI to render.
    properties.append(property)

    Task {
        do {
            // Create the property record
            let result = try await Amplify.API.mutate(request: .create(property))
            guard case .failure(let graphQLResponse) = result else {
                return
            }
            print("Failed with error: ", graphQLResponse)
            // Remove the newly created property
            if let index = properties.firstIndex(where: { $0?.id == property.id }) {
                properties.remove(at: index)
            }
        } catch {
            print("Failed with error: ", error)
            // Remove the newly created property
            if let index = properties.firstIndex(where: { $0?.id == property.id }) {
                properties.remove(at: index)
            }
        }
    }
}
```

## Optimistically rendering a record update

To optimistically render updates on a single item, use the code snippet like below:

```swift
func updateProperty(_ property: RealEstateProperty) async {
    guard let index = properties.firstIndex(where: { $0?.id == property.id }) else {
        print("No property to update")
        return
    }

    // Optimistically update the property, for the UI to render.
    let rollbackProperty = properties[index]
    properties[index] = property

    do {
        // Update the property record
        let result = try await Amplify.API.mutate(request: .update(property))
        guard case .failure(let graphQLResponse) = result else {
            return
        }
        print("Failed with error: ", graphQLResponse)
        properties[index] = rollbackProperty
    } catch {
        print("Failed with error: ", error)
        properties[index] = rollbackProperty
    }
}
```

## Optimistically render deleting a record

To optimistically render a Amplify Data API delete, use the code snippet like below:

```swift
func deleteProperty(_ property: RealEstateProperty) async {
  guard let index = properties.firstIndex(where: { $0?.id == property.id }) else {
      print("No property to remove")
      return
  }

  // Optimistically remove the property, for the UI to render.
  let rollbackProperty = properties[index]
  properties[index] = nil

  do {
      // Delete the property record
      let result = try await Amplify.API.mutate(request: .delete(property))
      switch result {
      case .success:
          // Finalize the removal
          properties.remove(at: index)
      case .failure(let graphQLResponse):
          print("Failed with error: ", graphQLResponse)
          // Undo the removal
          properties[index] = rollbackProperty
      }

  } catch {
      print("Failed with error: ", error)
      // Undo the removal
      properties[index] = rollbackProperty
  }
}
```

## Complete example

#### [Main]

```swift
import SwiftUI
import Amplify
import AWSAPIPlugin

@main
struct OptimisticUIApp: App {

    init() {
        do {
            Amplify.Logging.logLevel = .verbose
            try Amplify.add(plugin: AWSAPIPlugin(modelRegistration: AmplifyModels()))
            try Amplify.configure(with: .amplifyOutputs)
            print("Amplify configured with API, Storage, and Auth plugins!")
        } catch {
            print("Failed to initialize Amplify with \(error)")
        }
    }

    var body: some Scene {
        WindowGroup {
            RealEstatePropertyContainerView()
        }
    }
}

// Extend the model to Identifiable to make it compatible with SwiftUI's `ForEach`.
extension RealEstateProperty: Identifiable { }

struct TappedButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .padding(10)
            .background(configuration.isPressed ? Color.teal.opacity(0.8) : Color.teal)
            .foregroundColor(.white)
            .clipShape(RoundedRectangle(cornerRadius: 10))
    }
}
```

#### [Actor]

```swift
actor RealEstatePropertyList {

    private var properties: [RealEstateProperty?] = [] {
        didSet {
            subject.send(properties.compactMap { $0 })
        }
    }

    private let subject = PassthroughSubject<[RealEstateProperty], Never>()
    var publisher: AnyPublisher<[RealEstateProperty], Never> {
        subject.eraseToAnyPublisher()
    }

    func listProperties() async throws {
        let result = try await Amplify.API.query(request: .list(RealEstateProperty.self))
        guard case .success(let propertyList) = result else {
            print("Failed with error: ", result)
            return
        }
        properties = propertyList.elements
    }

    func createProperty(name: String, address: String? = nil) {
        let property = RealEstateProperty(name: name, address: address)
        // Optimistically send the newly created property, for the UI to render.
        properties.append(property)

        Task {
            do {
                // Create the property record
                let result = try await Amplify.API.mutate(request: .create(property))
                guard case .failure(let graphQLResponse) = result else {
                    return
                }
                print("Failed with error: ", graphQLResponse)
                // Remove the newly created property
                if let index = properties.firstIndex(where: { $0?.id == property.id }) {
                    properties.remove(at: index)
                }
            } catch {
                print("Failed with error: ", error)
                // Remove the newly created property
                if let index = properties.firstIndex(where: { $0?.id == property.id }) {
                    properties.remove(at: index)
                }
            }
        }
    }

    func updateProperty(_ property: RealEstateProperty) async {
        guard let index = properties.firstIndex(where: { $0?.id == property.id }) else {
            print("No property to update")
            return
        }

        // Optimistically update the property, for the UI to render.
        let rollbackProperty = properties[index]
        properties[index] = property

        do {
            // Update the property record
            let result = try await Amplify.API.mutate(request: .update(property))
            guard case .failure(let graphQLResponse) = result else {
                return
            }
            print("Failed with error: ", graphQLResponse)
            properties[index] = rollbackProperty
        } catch {
            print("Failed with error: ", error)
            properties[index] = rollbackProperty
        }
    }

    func deleteProperty(_ property: RealEstateProperty) async {
        guard let index = properties.firstIndex(where: { $0?.id == property.id }) else {
            print("No property to remove")
            return
        }

        // Optimistically remove the property, for the UI to render.
        let rollbackProperty = properties[index]
        properties[index] = nil

        do {
            // Delete the property record
            let result = try await Amplify.API.mutate(request: .delete(property))
            switch result {
            case .success:
                // Finalize the removal
                properties.remove(at: index)
            case .failure(let graphQLResponse):
                print("Failed with error: ", graphQLResponse)
                // Undo the removal
                properties[index] = rollbackProperty
            }

        } catch {
            print("Failed with error: ", error)
            // Undo the removal
            properties[index] = rollbackProperty
        }
    }
}

```

#### [View]

```swift
class RealEstatePropertyContainerViewModel: ObservableObject {
    @Published var properties: [RealEstateProperty] = []
    var sink: AnyCancellable?

    var propertyList = RealEstatePropertyList()
    init() {
        Task {
            sink = await propertyList.publisher
                .receive(on: DispatchQueue.main)
                .sink { properties in
                    print("Updating property list")
                    self.properties = properties
            }
        }
    }

    func loadList() {
        Task {
            try? await propertyList.listProperties()
        }
    }
    func createPropertyButtonTapped(name: String) {
        Task {
            await propertyList.createProperty(name: name)
        }
    }

    func updatePropertyButtonTapped(_ property: RealEstateProperty) {
        Task {
            await propertyList.updateProperty(property)
        }
    }

    func deletePropertyButtonTapped(_ property: RealEstateProperty) {
        Task {
            await propertyList.deleteProperty(property)
        }
    }
}

struct RealEstatePropertyContainerView: View {
    @StateObject var viewModel = RealEstatePropertyContainerViewModel()
    @State private var propertyName: String = ""

    var body: some View {
        VStack {
            ScrollView {
                LazyVStack(alignment: .leading) {
                    ForEach($viewModel.properties) { $property in
                        HStack {
                            TextField("Update property name", text: $property.name)
                                .textFieldStyle(RoundedBorderTextFieldStyle())
                                .multilineTextAlignment(.center)
                            Button("Update") {
                                viewModel.updatePropertyButtonTapped(property)
                            }
                            Button {
                                viewModel.deletePropertyButtonTapped(property)
                            } label: {
                                Image(systemName: "xmark")
                                    .foregroundColor(.red)
                            }

                        }.padding(.horizontal)
                    }
                }
            }.refreshable {
                viewModel.loadList()
            }
            TextField("New property name", text: $propertyName)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .multilineTextAlignment(.center)

            Button("Save") {
                viewModel.createPropertyButtonTapped(name: propertyName)
                self.propertyName = ""
            }
            .buttonStyle(TappedButtonStyle())
        }.task {
            viewModel.loadList()
        }
    }
}

struct RealEstatePropertyContainerView_Previews: PreviewProvider {
    static var previews: some View {
        RealEstatePropertyContainerView()
    }
}
```

<!-- /Platform -->
