Page updated Jan 16, 2024

Optimistic UI

The GraphQL API category can be used with TanStack Query to implement optimistic UI, allowing CRUD operations to be rendered immediately on the UI before the request roundtrip has completed. Using the GraphQL API 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.

For more details on TanStack Query, including requirements, supported browsers, and advanced usage, see the TanStack Query documentation. For complete guidance on how to implement optimistic updates with TanStack Query, see the TanStack Query Optimistic UI Documentation. For more on the Amplify GraphQL API, see the API documentation.

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

1# Install TanStack Query
2npm i @tanstack/react-query
3
4# Select default configuration
5amplify add api

When prompted, use the following schema:

1type RealEstateProperty @model @auth(rules: [{ allow: public }]) {
2 id: ID!
3 name: String!
4 address: String
5}

The schema file can also be found under amplify/backend/api/[name of project]/schema.graphql. Save the schema and run amplify push to deploy the changes. 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:

1import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
3
4// Create a client
5const queryClient = new QueryClient()

Next, wrap your app in the client provider:

1<QueryClientProvider client={queryClient}>
2 <App />
3 <ReactQueryDevtools initialIsOpen={false} />
4</QueryClientProvider>

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

For the complete working example, including required imports and React component state management, see the Complete Example below.

How to use TanStack Query query keys with the GraphQL 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 the GraphQL API, it is important to note that different query keys must be used 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.

Optimistically rendering a list of records

To optimistically render a list of items returned from the GraphQL API, use the TanStack useQuery hook, passing in the GraphQL 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.

1const {
2 data: realEstateProperties,
3 isLoading,
4 isSuccess,
5 isError: isErrorQuery,
6} = useQuery({
7 queryKey: ["realEstateProperties"],
8 queryFn: async () => {
9 const response = await API.graphql<
10 GraphQLQuery<ListRealEstatePropertiesQuery>
11 >({
12 query: queries.listRealEstateProperties,
13 });
14
15 const allRealEstateProperties =
16 response?.data?.listRealEstateProperties?.items;
17
18 if (!allRealEstateProperties) return null;
19
20 return allRealEstateProperties;
21 },
22});
1const {
2 data: realEstateProperties,
3 isLoading,
4 isSuccess,
5 isError: isErrorQuery,
6} = useQuery({
7 queryKey: ["realEstateProperties"],
8 queryFn: async () => {
9 const response = await API.graphql({
10 query: queries.listRealEstateProperties,
11 });
12
13 const allRealEstateProperties =
14 response?.data?.listRealEstateProperties?.items;
15
16 if (!allRealEstateProperties) return null;
17
18 return allRealEstateProperties;
19 },
20});

Optimistically rendering a newly created record

To optimistically render a newly created record returned from the GraphQL API, use the TanStack useMutation hook, passing in the GraphQL 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.

1const createMutation = useMutation({
2 mutationFn: async (
3 realEstatePropertyDetails: CreateRealEstatePropertyInput
4 ) => {
5 const response = await API.graphql<
6 GraphQLQuery<CreateRealEstatePropertyMutation>
7 >({
8 query: mutations.createRealEstateProperty,
9 variables: { input: realEstatePropertyDetails },
10 });
11
12 const newRealEstateProperty = response?.data?.createRealEstateProperty;
13 return newRealEstateProperty;
14 },
15 // When mutate is called:
16 onMutate: async (newRealEstateProperty) => {
17 // Cancel any outgoing refetches
18 // (so they don't overwrite our optimistic update)
19 await queryClient.cancelQueries({ queryKey: ["realEstateProperties"] });
20
21 // Snapshot the previous value
22 const previousRealEstateProperties = queryClient.getQueryData([
23 "realEstateProperties",
24 ]);
25
26 // Optimistically update to the new value
27 if (previousRealEstateProperties) {
28 queryClient.setQueryData(["realEstateProperties"], (old: any) => [
29 ...old,
30 newRealEstateProperty,
31 ]);
32 }
33
34 // Return a context object with the snapshotted value
35 return { previousRealEstateProperties };
36 },
37 // If the mutation fails,
38 // use the context returned from onMutate to rollback
39 onError: (err, newRealEstateProperty, context) => {
40 console.error("Error saving record:", err, newRealEstateProperty);
41 if (context?.previousRealEstateProperties) {
42 queryClient.setQueryData(
43 ["realEstateProperties"],
44 context.previousRealEstateProperties
45 );
46 }
47 },
48 // Always refetch after error or success:
49 onSettled: () => {
50 queryClient.invalidateQueries({ queryKey: ["realEstateProperties"] });
51 },
52});
1const createMutation = useMutation({
2 mutationFn: async (realEstatePropertyDetails) => {
3 const response = await API.graphql({
4 query: mutations.createRealEstateProperty,
5 variables: { input: realEstatePropertyDetails },
6 });
7
8 const newRealEstateProperty = response?.data?.createRealEstateProperty;
9 return newRealEstateProperty;
10 },
11 // When mutate is called:
12 onMutate: async (newRealEstateProperty) => {
13 // Cancel any outgoing refetches
14 // (so they don't overwrite our optimistic update)
15 await queryClient.cancelQueries({ queryKey: ["realEstateProperties"] });
16
17 // Snapshot the previous value
18 const previousRealEstateProperties = queryClient.getQueryData([
19 "realEstateProperties",
20 ]);
21
22 // Optimistically update to the new value
23 if (previousRealEstateProperties) {
24 queryClient.setQueryData(["realEstateProperties"], (old) => [
25 ...old,
26 newRealEstateProperty,
27 ]);
28 }
29
30 // Return a context object with the snapshotted value
31 return { previousRealEstateProperties };
32 },
33 // If the mutation fails,
34 // use the context returned from onMutate to rollback
35 onError: (err, newRealEstateProperty, context) => {
36 console.error("Error saving record:", err, newRealEstateProperty);
37 if (context?.previousRealEstateProperties) {
38 queryClient.setQueryData(
39 ["realEstateProperties"],
40 context.previousRealEstateProperties
41 );
42 }
43 },
44 // Always refetch after error or success:
45 onSettled: () => {
46 queryClient.invalidateQueries({ queryKey: ["realEstateProperties"] });
47 },
48});

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 GraphQL API query as the queryFn parameter. For the query key, we'll use a combination of realEstateProperties and the record's unique identifier.

1const {
2 data: realEstateProperty,
3 isLoading,
4 isSuccess,
5 isError: isErrorQuery,
6} = useQuery({
7 queryKey: ["realEstateProperties", currentRealEstatePropertyId],
8 queryFn: async () => {
9 const response = await API.graphql<
10 GraphQLQuery<GetRealEstatePropertyQuery>
11 >({
12 query: queries.getRealEstateProperty,
13 variables: { id: currentRealEstatePropertyId },
14 });
15
16 return response.data?.getRealEstateProperty;
17 },
18});
1const {
2 data: realEstateProperty,
3 isLoading,
4 isSuccess,
5 isError: isErrorQuery,
6} = useQuery({
7 queryKey: ["realEstateProperties", currentRealEstatePropertyId],
8 queryFn: async () => {
9 const response = await API.graphql({
10 query: queries.getRealEstateProperty,
11 variables: { id: currentRealEstatePropertyId },
12 });
13
14 return response.data?.getRealEstateProperty;
15 },
16});

Optimistically render updates for a record

To optimistically render GraphQL API updates for a single record, use the TanStack useMutation hook, passing in the GraphQL API 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.

When directly interacting with the cache via the onMutate function, it should be noted that the newRealEstateProperty parameter only includes the fields that are being updated, until the final return from the GraphQL API returns all fields for the record. 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.

1const updateMutation = useMutation({
2 mutationFn: async (
3 realEstatePropertyDetails: UpdateRealEstatePropertyInput
4 ) => {
5 const response = await API.graphql<
6 GraphQLQuery<UpdateRealEstatePropertyMutation>
7 >({
8 query: mutations.updateRealEstateProperty,
9 variables: { input: realEstatePropertyDetails },
10 });
11
12 return response?.data?.updateRealEstateProperty;
13 },
14 // When mutate is called:
15 onMutate: async (newRealEstateProperty) => {
16 // Cancel any outgoing refetches
17 // (so they don't overwrite our optimistic update)
18 await queryClient.cancelQueries({
19 queryKey: ["realEstateProperties", newRealEstateProperty.id],
20 });
21
22 await queryClient.cancelQueries({
23 queryKey: ["realEstateProperties"],
24 });
25
26 // Snapshot the previous value
27 const previousRealEstateProperty = queryClient.getQueryData([
28 "realEstateProperties",
29 newRealEstateProperty.id,
30 ]);
31
32 // Optimistically update to the new value
33 if (previousRealEstateProperty) {
34 queryClient.setQueryData(
35 ["realEstateProperties", newRealEstateProperty.id],
36 /**
37 * `newRealEstateProperty` will at first only include updated values for
38 * the record. To avoid only rendering optimistic values for updated
39 * fields on the UI, include the previous values for all fields:
40 */
41 { ...previousRealEstateProperty, ...newRealEstateProperty }
42 );
43 }
44
45 // Return a context with the previous and new realEstateProperty
46 return { previousRealEstateProperty, newRealEstateProperty };
47 },
48 // If the mutation fails, use the context we returned above
49 onError: (err, newRealEstateProperty, context) => {
50 console.error("Error updating record:", err, newRealEstateProperty);
51 if (context?.previousRealEstateProperty) {
52 queryClient.setQueryData(
53 ["realEstateProperties", context.newRealEstateProperty.id],
54 context.previousRealEstateProperty
55 );
56 }
57 },
58 // Always refetch after error or success:
59 onSettled: (newRealEstateProperty) => {
60 if (newRealEstateProperty) {
61 queryClient.invalidateQueries({
62 queryKey: ["realEstateProperties", newRealEstateProperty.id],
63 });
64 queryClient.invalidateQueries({
65 queryKey: ["realEstateProperties"],
66 });
67 }
68 },
69});
1const updateMutation = useMutation({
2 mutationFn: async (realEstatePropertyDetails) => {
3 const response = await API.graphql({
4 query: mutations.updateRealEstateProperty,
5 variables: { input: realEstatePropertyDetails },
6 });
7
8 return response?.data?.updateRealEstateProperty;
9 },
10 // When mutate is called:
11 onMutate: async (newRealEstateProperty) => {
12 // Cancel any outgoing refetches
13 // (so they don't overwrite our optimistic update)
14 await queryClient.cancelQueries({
15 queryKey: ["realEstateProperties", newRealEstateProperty.id],
16 });
17
18 await queryClient.cancelQueries({
19 queryKey: ["realEstateProperties"],
20 });
21
22 // Snapshot the previous value
23 const previousRealEstateProperty = queryClient.getQueryData([
24 "realEstateProperties",
25 newRealEstateProperty.id,
26 ]);
27
28 // Optimistically update to the new value
29 if (previousRealEstateProperty) {
30 queryClient.setQueryData(
31 ["realEstateProperties", newRealEstateProperty.id],
32 /**
33 * `newRealEstateProperty` will at first only include updated values for
34 * the record. To avoid only rendering optimistic values for updated
35 * fields on the UI, include the previous values for all fields:
36 */
37 { ...previousRealEstateProperty, ...newRealEstateProperty }
38 );
39 }
40
41 // Return a context with the previous and new realEstateProperty
42 return { previousRealEstateProperty, newRealEstateProperty };
43 },
44 // If the mutation fails, use the context we returned above
45 onError: (err, newRealEstateProperty, context) => {
46 console.error("Error updating record:", err, newRealEstateProperty);
47 if (context?.previousRealEstateProperty) {
48 queryClient.setQueryData(
49 ["realEstateProperties", context.newRealEstateProperty.id],
50 context.previousRealEstateProperty
51 );
52 }
53 },
54 // Always refetch after error or success:
55 onSettled: (newRealEstateProperty) => {
56 if (newRealEstateProperty) {
57 queryClient.invalidateQueries({
58 queryKey: ["realEstateProperties", newRealEstateProperty.id],
59 });
60 queryClient.invalidateQueries({
61 queryKey: ["realEstateProperties"],
62 });
63 }
64 },
65});

Optimistically render deleting a record

To optimistically render a GraphQL API delete of a single record, use the TanStack useMutation hook, passing in the GraphQL API 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.

1const deleteMutation = useMutation({
2 mutationFn: async (
3 realEstatePropertyDetails: DeleteRealEstatePropertyInput
4 ) => {
5 const response = await API.graphql<
6 GraphQLQuery<DeleteRealEstatePropertyMutation>
7 >({
8 query: mutations.deleteRealEstateProperty,
9 variables: { input: realEstatePropertyDetails },
10 });
11
12 return response?.data?.deleteRealEstateProperty;
13 },
14 // When mutate is called:
15 onMutate: async (newRealEstateProperty) => {
16 // Cancel any outgoing refetches
17 // (so they don't overwrite our optimistic update)
18 await queryClient.cancelQueries({
19 queryKey: ["realEstateProperties", newRealEstateProperty.id],
20 });
21
22 await queryClient.cancelQueries({
23 queryKey: ["realEstateProperties"],
24 });
25
26 // Snapshot the previous value
27 const previousRealEstateProperty = queryClient.getQueryData([
28 "realEstateProperties",
29 newRealEstateProperty.id,
30 ]);
31
32 // Optimistically update to the new value
33 if (previousRealEstateProperty) {
34 queryClient.setQueryData(
35 ["realEstateProperties", newRealEstateProperty.id],
36 newRealEstateProperty
37 );
38 }
39
40 // Return a context with the previous and new realEstateProperty
41 return { previousRealEstateProperty, newRealEstateProperty };
42 },
43 // If the mutation fails, use the context we returned above
44 onError: (err, newRealEstateProperty, context) => {
45 console.error("Error deleting record:", err, newRealEstateProperty);
46 if (context?.previousRealEstateProperty) {
47 queryClient.setQueryData(
48 ["realEstateProperties", context.newRealEstateProperty.id],
49 context.previousRealEstateProperty
50 );
51 }
52 },
53 // Always refetch after error or success:
54 onSettled: (newRealEstateProperty) => {
55 if (newRealEstateProperty) {
56 queryClient.invalidateQueries({
57 queryKey: ["realEstateProperties", newRealEstateProperty.id],
58 });
59 queryClient.invalidateQueries({
60 queryKey: ["realEstateProperties"],
61 });
62 }
63 },
64});
1const deleteMutation = useMutation({
2 mutationFn: async (realEstatePropertyDetails) => {
3 const response = await API.graphql({
4 query: mutations.deleteRealEstateProperty,
5 variables: { input: realEstatePropertyDetails },
6 });
7
8 return response?.data?.deleteRealEstateProperty;
9 },
10 // When mutate is called:
11 onMutate: async (newRealEstateProperty) => {
12 // Cancel any outgoing refetches
13 // (so they don't overwrite our optimistic update)
14 await queryClient.cancelQueries({
15 queryKey: ["realEstateProperties", newRealEstateProperty.id],
16 });
17
18 await queryClient.cancelQueries({
19 queryKey: ["realEstateProperties"],
20 });
21
22 // Snapshot the previous value
23 const previousRealEstateProperty = queryClient.getQueryData([
24 "realEstateProperties",
25 newRealEstateProperty.id,
26 ]);
27
28 // Optimistically update to the new value
29 if (previousRealEstateProperty) {
30 queryClient.setQueryData(
31 ["realEstateProperties", newRealEstateProperty.id],
32 newRealEstateProperty
33 );
34 }
35
36 // Return a context with the previous and new realEstateProperty
37 return { previousRealEstateProperty, newRealEstateProperty };
38 },
39 // If the mutation fails, use the context we returned above
40 onError: (err, newRealEstateProperty, context) => {
41 console.error("Error deleting record:", err, newRealEstateProperty);
42 if (context?.previousRealEstateProperty) {
43 queryClient.setQueryData(
44 ["realEstateProperties", context.newRealEstateProperty.id],
45 context.previousRealEstateProperty
46 );
47 }
48 },
49 // Always refetch after error or success:
50 onSettled: (newRealEstateProperty) => {
51 if (newRealEstateProperty) {
52 queryClient.invalidateQueries({
53 queryKey: ["realEstateProperties", newRealEstateProperty.id],
54 });
55 queryClient.invalidateQueries({
56 queryKey: ["realEstateProperties"],
57 });
58 }
59 },
60});

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. For the purposes of this demo, we show a global loading indicator in the Complete Example when any queries are fetching (including in the background) in order to help visualize what TanStack is doing in the background:

1function GlobalLoadingIndicator() {
2 const isFetching = useIsFetching();
3 return isFetching ? <div style={styles.globalLoadingIndicator}></div> : null;
4}

For more details on advanced usage of TanStack Query hooks, see the TanStack documentation.

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 below.

1<>
2 {updateMutation.isError &&
3 updateMutation.error instanceof Error ? (
4 <div>An error occurred: {updateMutation.error.message}</div>
5 ) : null}
6
7 {updateMutation.isSuccess ? (
8 <div>Real Estate Property updated!</div>
9 ) : null}
10
11 <button
12 onClick={() =>
13 updateMutation.mutate({
14 id: realEstateProperty.id,
15 address: `${Math.floor(
16 1000 + Math.random() * 9000
17 )} Main St`,
18 })
19 }
20 >
21 Update Address
22 </button>
23</>
1<>
2 {updateMutation.isError ? (
3 <div>An error occurred: {updateMutation.error.message}</div>
4 ) : null}
5
6 {updateMutation.isSuccess ? (
7 <div>Real Estate Property updated!</div>
8 ) : null}
9
10 <button
11 onClick={() =>
12 updateMutation.mutate({
13 id: realEstateProperty.id,
14 address: `${Math.floor(
15 1000 + Math.random() * 9000
16 )} Main St`,
17 })
18 }
19 >
20 Update Address
21 </button>
22</>

Complete example

1// index.tsx:
2import React from "react";
3import ReactDOM from "react-dom/client";
4import "./index.css";
5import App from "./App";
6import { Amplify } from "aws-amplify";
7import awsExports from "./aws-exports";
8import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
9import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
10import "@aws-amplify/ui-react/styles.css";
11
12Amplify.configure(awsExports);
13
14// Create a client
15const queryClient = new QueryClient();
16
17const root = ReactDOM.createRoot(
18 document.getElementById("root") as HTMLElement
19);
20
21// Provide the client to your App
22root.render(
23 <React.StrictMode>
24 <QueryClientProvider client={queryClient}>
25 <App />
26 <ReactQueryDevtools initialIsOpen={false} />
27 </QueryClientProvider>
28 </React.StrictMode>
29);
30
31// App.tsx:
32import React, { useState } from "react";
33import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
34import { useIsFetching } from "@tanstack/react-query";
35import { API } from "aws-amplify";
36import * as mutations from "./graphql/mutations";
37import * as queries from "./graphql/queries";
38import { GraphQLQuery } from "@aws-amplify/api";
39import {
40 CreateRealEstatePropertyInput,
41 CreateRealEstatePropertyMutation,
42 DeleteRealEstatePropertyInput,
43 DeleteRealEstatePropertyMutation,
44 GetRealEstatePropertyQuery,
45 ListRealEstatePropertiesQuery,
46 RealEstateProperty,
47 UpdateRealEstatePropertyInput,
48 UpdateRealEstatePropertyMutation,
49} from "./API";
50
51/**
52 * https://www.tanstack.com/query/v4/docs/react/guides/background-fetching-indicators#displaying-global-background-fetching-loading-state
53 * For the purposes of this demo, we show a global loading indicator when *any*
54 * queries are fetching (including in the background) in order to help visualize
55 * what TanStack is doing in the background. This example also displays
56 * indicators for individual query and mutation loading states.
57 */
58function GlobalLoadingIndicator() {
59 const isFetching = useIsFetching();
60
61 return isFetching ? <div style={styles.globalLoadingIndicator}></div> : null;
62}
63
64function App() {
65 const [currentRealEstatePropertyId, setCurrentRealEstatePropertyId] =
66 useState<string | null>(null);
67
68 // Access the client
69 const queryClient = useQueryClient();
70
71 // TanStack Query for listing all real estate properties:
72 const {
73 data: realEstateProperties,
74 isLoading,
75 isSuccess,
76 isError: isErrorQuery,
77 } = useQuery({
78 queryKey: ["realEstateProperties"],
79 queryFn: async () => {
80 const response = await API.graphql<
81 GraphQLQuery<ListRealEstatePropertiesQuery>
82 >({
83 query: queries.listRealEstateProperties,
84 });
85
86 const allRealEstateProperties =
87 response?.data?.listRealEstateProperties?.items;
88
89 if (!allRealEstateProperties) return null;
90
91 return allRealEstateProperties;
92 },
93 });
94
95 // TanStack create mutation with optimistic updates
96 const createMutation = useMutation({
97 mutationFn: async (
98 realEstatePropertyDetails: CreateRealEstatePropertyInput
99 ) => {
100 const response = await API.graphql<
101 GraphQLQuery<CreateRealEstatePropertyMutation>
102 >({
103 query: mutations.createRealEstateProperty,
104 variables: { input: realEstatePropertyDetails },
105 });
106
107 const newRealEstateProperty = response?.data?.createRealEstateProperty;
108 return newRealEstateProperty;
109 },
110 // When mutate is called:
111 onMutate: async (newRealEstateProperty) => {
112 // Cancel any outgoing refetches
113 // (so they don't overwrite our optimistic update)
114 await queryClient.cancelQueries({ queryKey: ["realEstateProperties"] });
115
116 // Snapshot the previous value
117 const previousRealEstateProperties = queryClient.getQueryData([
118 "realEstateProperties",
119 ]);
120
121 // Optimistically update to the new value
122 if (previousRealEstateProperties) {
123 queryClient.setQueryData(["realEstateProperties"], (old: any) => [
124 ...old,
125 newRealEstateProperty,
126 ]);
127 }
128
129 // Return a context object with the snapshotted value
130 return { previousRealEstateProperties };
131 },
132 // If the mutation fails,
133 // use the context returned from onMutate to rollback
134 onError: (err, newRealEstateProperty, context) => {
135 console.error("Error saving record:", err, newRealEstateProperty);
136 if (context?.previousRealEstateProperties) {
137 queryClient.setQueryData(
138 ["realEstateProperties"],
139 context.previousRealEstateProperties
140 );
141 }
142 },
143 // Always refetch after error or success:
144 onSettled: () => {
145 queryClient.invalidateQueries({ queryKey: ["realEstateProperties"] });
146 },
147 });
148
149 /**
150 * Note: this example does not return to the list view on delete in order
151 * to demonstrate the optimistic update.
152 */
153 function RealEstatePropertyDetailView() {
154 const {
155 data: realEstateProperty,
156 isLoading,
157 isSuccess,
158 isError: isErrorQuery,
159 } = useQuery({
160 queryKey: ["realEstateProperties", currentRealEstatePropertyId],
161 queryFn: async () => {
162 const response = await API.graphql<
163 GraphQLQuery<GetRealEstatePropertyQuery>
164 >({
165 query: queries.getRealEstateProperty,
166 variables: { id: currentRealEstatePropertyId },
167 });
168
169 return response.data?.getRealEstateProperty;
170 },
171 });
172
173 // TanStack update mutation with optimistic updates
174 const updateMutation = useMutation({
175 mutationFn: async (
176 realEstatePropertyDetails: UpdateRealEstatePropertyInput
177 ) => {
178 const response = await API.graphql<
179 GraphQLQuery<UpdateRealEstatePropertyMutation>
180 >({
181 query: mutations.updateRealEstateProperty,
182 variables: { input: realEstatePropertyDetails },
183 });
184
185 return response?.data?.updateRealEstateProperty;
186 },
187 // When mutate is called:
188 onMutate: async (newRealEstateProperty) => {
189 // Cancel any outgoing refetches
190 // (so they don't overwrite our optimistic update)
191 await queryClient.cancelQueries({
192 queryKey: ["realEstateProperties", newRealEstateProperty.id],
193 });
194
195 await queryClient.cancelQueries({
196 queryKey: ["realEstateProperties"],
197 });
198
199 // Snapshot the previous value
200 const previousRealEstateProperty = queryClient.getQueryData([
201 "realEstateProperties",
202 newRealEstateProperty.id,
203 ]);
204
205 // Optimistically update to the new value
206 if (previousRealEstateProperty) {
207 queryClient.setQueryData(
208 ["realEstateProperties", newRealEstateProperty.id],
209 /**
210 * `newRealEstateProperty` will at first only include updated values for
211 * the record. To avoid only rendering optimistic values for updated
212 * fields on the UI, include the previous values for all fields:
213 */
214 { ...previousRealEstateProperty, ...newRealEstateProperty }
215 );
216 }
217
218 // Return a context with the previous and new realEstateProperty
219 return { previousRealEstateProperty, newRealEstateProperty };
220 },
221 // If the mutation fails, use the context we returned above
222 onError: (err, newRealEstateProperty, context) => {
223 console.error("Error updating record:", err, newRealEstateProperty);
224 if (context?.previousRealEstateProperty) {
225 queryClient.setQueryData(
226 ["realEstateProperties", context.newRealEstateProperty.id],
227 context.previousRealEstateProperty
228 );
229 }
230 },
231 // Always refetch after error or success:
232 onSettled: (newRealEstateProperty) => {
233 if (newRealEstateProperty) {
234 queryClient.invalidateQueries({
235 queryKey: ["realEstateProperties", newRealEstateProperty.id],
236 });
237 queryClient.invalidateQueries({
238 queryKey: ["realEstateProperties"],
239 });
240 }
241 },
242 });
243
244 // TanStack delete mutation with optimistic updates
245 const deleteMutation = useMutation({
246 mutationFn: async (
247 realEstatePropertyDetails: DeleteRealEstatePropertyInput
248 ) => {
249 const response = await API.graphql<
250 GraphQLQuery<DeleteRealEstatePropertyMutation>
251 >({
252 query: mutations.deleteRealEstateProperty,
253 variables: { input: realEstatePropertyDetails },
254 });
255
256 return response?.data?.deleteRealEstateProperty;
257 },
258 // When mutate is called:
259 onMutate: async (newRealEstateProperty) => {
260 // Cancel any outgoing refetches
261 // (so they don't overwrite our optimistic update)
262 await queryClient.cancelQueries({
263 queryKey: ["realEstateProperties", newRealEstateProperty.id],
264 });
265
266 await queryClient.cancelQueries({
267 queryKey: ["realEstateProperties"],
268 });
269
270 // Snapshot the previous value
271 const previousRealEstateProperty = queryClient.getQueryData([
272 "realEstateProperties",
273 newRealEstateProperty.id,
274 ]);
275
276 // Optimistically update to the new value
277 if (previousRealEstateProperty) {
278 queryClient.setQueryData(
279 ["realEstateProperties", newRealEstateProperty.id],
280 newRealEstateProperty
281 );
282 }
283
284 // Return a context with the previous and new realEstateProperty
285 return { previousRealEstateProperty, newRealEstateProperty };
286 },
287 // If the mutation fails, use the context we returned above
288 onError: (err, newRealEstateProperty, context) => {
289 console.error("Error deleting record:", err, newRealEstateProperty);
290 if (context?.previousRealEstateProperty) {
291 queryClient.setQueryData(
292 ["realEstateProperties", context.newRealEstateProperty.id],
293 context.previousRealEstateProperty
294 );
295 }
296 },
297 // Always refetch after error or success:
298 onSettled: (newRealEstateProperty) => {
299 if (newRealEstateProperty) {
300 queryClient.invalidateQueries({
301 queryKey: ["realEstateProperties", newRealEstateProperty.id],
302 });
303 queryClient.invalidateQueries({
304 queryKey: ["realEstateProperties"],
305 });
306 }
307 },
308 });
309
310 return (
311 <div style={styles.detailViewContainer}>
312 <h2>Real Estate Property Detail View</h2>
313 {isErrorQuery && <div>{"Problem loading Real Estate Property"}</div>}
314 {isLoading && (
315 <div style={styles.loadingIndicator}>
316 {"Loading Real Estate Property..."}
317 </div>
318 )}
319 {isSuccess && (
320 <div>
321 <p>{`Name: ${realEstateProperty?.name}`}</p>
322 <p>{`Address: ${realEstateProperty?.address}`}</p>
323 </div>
324 )}
325 {realEstateProperty && (
326 <div>
327 <div>
328 {updateMutation.isLoading ? (
329 "Updating Real Estate Property..."
330 ) : (
331 <>
332 {updateMutation.isError &&
333 updateMutation.error instanceof Error ? (
334 <div>An error occurred: {updateMutation.error.message}</div>
335 ) : null}
336
337 {updateMutation.isSuccess ? (
338 <div>Real Estate Property updated!</div>
339 ) : null}
340
341 <button
342 onClick={() =>
343 updateMutation.mutate({
344 id: realEstateProperty.id,
345 name: `Updated Home ${Date.now()}`,
346 })
347 }
348 >
349 Update Name
350 </button>
351 <button
352 onClick={() =>
353 updateMutation.mutate({
354 id: realEstateProperty.id,
355 address: `${Math.floor(
356 1000 + Math.random() * 9000
357 )} Main St`,
358 })
359 }
360 >
361 Update Address
362 </button>
363 </>
364 )}
365 </div>
366
367 <div>
368 {deleteMutation.isLoading ? (
369 "Deleting Real Estate Property..."
370 ) : (
371 <>
372 {deleteMutation.isError &&
373 deleteMutation.error instanceof Error ? (
374 <div>An error occurred: {deleteMutation.error.message}</div>
375 ) : null}
376
377 {deleteMutation.isSuccess ? (
378 <div>Real Estate Property deleted!</div>
379 ) : null}
380
381 <button
382 onClick={() =>
383 deleteMutation.mutate({
384 id: realEstateProperty.id,
385 })
386 }
387 >
388 Delete
389 </button>
390 </>
391 )}
392 </div>
393 </div>
394 )}
395 <button onClick={() => setCurrentRealEstatePropertyId(null)}>
396 Back
397 </button>
398 </div>
399 );
400 }
401
402 return (
403 <div>
404 {!currentRealEstatePropertyId && (
405 <div style={styles.appContainer}>
406 <h1>Real Estate Properties:</h1>
407 <div>
408 {createMutation.isLoading ? (
409 "Adding Real Estate Property..."
410 ) : (
411 <>
412 {createMutation.isError &&
413 createMutation.error instanceof Error ? (
414 <div>An error occurred: {createMutation.error.message}</div>
415 ) : null}
416
417 {createMutation.isSuccess ? (
418 <div>Real Estate Property added!</div>
419 ) : null}
420
421 <button
422 onClick={() => {
423 createMutation.mutate({
424 name: `New Home ${Date.now()}`,
425 address: `${Math.floor(
426 1000 + Math.random() * 9000
427 )} Main St`,
428 });
429 }}
430 >
431 Add RealEstateProperty
432 </button>
433 </>
434 )}
435 </div>
436 <ul style={styles.propertiesList}>
437 {isLoading && (
438 <div style={styles.loadingIndicator}>
439 {"Loading Real Estate Properties..."}
440 </div>
441 )}
442 {isErrorQuery && (
443 <div>{"Problem loading Real Estate Properties"}</div>
444 )}
445 {isSuccess &&
446 realEstateProperties?.map((realEstateProperty, idx) => {
447 if (!realEstateProperty) return null;
448 return (
449 <li
450 style={styles.listItem}
451 key={`${idx}-${realEstateProperty.id}`}
452 >
453 <p>{realEstateProperty.name}</p>
454 <button
455 style={styles.detailViewButton}
456 onClick={() =>
457 setCurrentRealEstatePropertyId(realEstateProperty.id)
458 }
459 >
460 Detail View
461 </button>
462 </li>
463 );
464 })}
465 </ul>
466 </div>
467 )}
468 {currentRealEstatePropertyId && <RealEstatePropertyDetailView />}
469 <GlobalLoadingIndicator />
470 </div>
471 );
472}
473
474export default App;
475
476const styles: any = {
477 appContainer: {
478 display: "flex",
479 flexDirection: "column",
480 alignItems: "center",
481 },
482 detailViewButton: { marginLeft: "1rem" },
483 detailViewContainer: { border: "1px solid black", padding: "3rem" },
484 globalLoadingIndicator: {
485 position: "fixed",
486 top: 0,
487 left: 0,
488 width: "100%",
489 height: "100%",
490 border: "4px solid blue",
491 pointerEvents: "none",
492 },
493 listItem: {
494 display: "flex",
495 justifyContent: "space-between",
496 border: "1px dotted grey",
497 padding: ".5rem",
498 margin: ".1rem",
499 },
500 loadingIndicator: {
501 border: "1px solid black",
502 padding: "1rem",
503 margin: "1rem",
504 },
505 propertiesList: {
506 display: "flex",
507 flexDirection: "column",
508 alignItems: "center",
509 justifyContent: "start",
510 width: "50%",
511 border: "1px solid black",
512 padding: "1rem",
513 listStyleType: "none",
514 },
515};
1// index.jsx
2import React from "react";
3import ReactDOM from "react-dom/client";
4import "./index.css";
5import App from "./App";
6import { Amplify } from "aws-amplify";
7import awsExports from "./aws-exports";
8import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
9import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
10import "@aws-amplify/ui-react/styles.css";
11
12Amplify.configure(awsExports);
13
14// Create a client
15const queryClient = new QueryClient();
16
17const root = ReactDOM.createRoot(document.getElementById("root"));
18
19// Provide the client to your App
20root.render(
21 <React.StrictMode>
22 <QueryClientProvider client={queryClient}>
23 <App />
24 <ReactQueryDevtools initialIsOpen={false} />
25 </QueryClientProvider>
26 </React.StrictMode>
27);
28
29// App.jsx
30import React, { useState } from "react";
31import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
32import { useIsFetching } from "@tanstack/react-query";
33import { API } from "aws-amplify";
34import * as mutations from "./graphql/mutations";
35import * as queries from "./graphql/queries";
36
37/**
38 * https://www.tanstack.com/query/v4/docs/react/guides/background-fetching-indicators#displaying-global-background-fetching-loading-state
39 * For the purposes of this demo, we show a global loading indicator when *any*
40 * queries are fetching (including in the background) in order to help visualize
41 * what TanStack is doing in the background. This example also displays
42 * indicators for individual query and mutation loading states.
43 */
44function GlobalLoadingIndicator() {
45 const isFetching = useIsFetching();
46
47 return isFetching ? <div style={styles.globalLoadingIndicator}></div> : null;
48}
49
50function App() {
51 const [currentRealEstatePropertyId, setCurrentRealEstatePropertyId] =
52 useState(null);
53
54 // Access the client
55 const queryClient = useQueryClient();
56
57 // TanStack Query for listing all real estate properties:
58 const {
59 data: realEstateProperties,
60 isLoading,
61 isSuccess,
62 isError: isErrorQuery,
63 } = useQuery({
64 queryKey: ["realEstateProperties"],
65 queryFn: async () => {
66 const response = await API.graphql({
67 query: queries.listRealEstateProperties,
68 });
69
70 const allRealEstateProperties =
71 response?.data?.listRealEstateProperties?.items;
72
73 if (!allRealEstateProperties) return null;
74
75 return allRealEstateProperties;
76 },
77 });
78
79 // TanStack create mutation with optimistic updates
80 const createMutation = useMutation({
81 mutationFn: async (realEstatePropertyDetails) => {
82 const response = await API.graphql({
83 query: mutations.createRealEstateProperty,
84 variables: { input: realEstatePropertyDetails },
85 });
86
87 const newRealEstateProperty = response?.data?.createRealEstateProperty;
88 return newRealEstateProperty;
89 },
90 // When mutate is called:
91 onMutate: async (newRealEstateProperty) => {
92 // Cancel any outgoing refetches
93 // (so they don't overwrite our optimistic update)
94 await queryClient.cancelQueries({ queryKey: ["realEstateProperties"] });
95
96 // Snapshot the previous value
97 const previousRealEstateProperties = queryClient.getQueryData([
98 "realEstateProperties",
99 ]);
100
101 // Optimistically update to the new value
102 if (previousRealEstateProperties) {
103 queryClient.setQueryData(["realEstateProperties"], (old) => [
104 ...old,
105 newRealEstateProperty,
106 ]);
107 }
108
109 // Return a context object with the snapshotted value
110 return { previousRealEstateProperties };
111 },
112 // If the mutation fails,
113 // use the context returned from onMutate to rollback
114 onError: (err, newRealEstateProperty, context) => {
115 console.error("Error saving record:", err, newRealEstateProperty);
116 if (context?.previousRealEstateProperties) {
117 queryClient.setQueryData(
118 ["realEstateProperties"],
119 context.previousRealEstateProperties
120 );
121 }
122 },
123 // Always refetch after error or success:
124 onSettled: () => {
125 queryClient.invalidateQueries({ queryKey: ["realEstateProperties"] });
126 },
127 });
128
129 /**
130 * Note: this example does not return to the list view on delete in order
131 * to demonstrate the optimistic update.
132 */
133 function RealEstatePropertyDetailView() {
134 const {
135 data: realEstateProperty,
136 isLoading,
137 isSuccess,
138 isError: isErrorQuery,
139 } = useQuery({
140 queryKey: ["realEstateProperties", currentRealEstatePropertyId],
141 queryFn: async () => {
142 const response = await API.graphql({
143 query: queries.getRealEstateProperty,
144 variables: { id: currentRealEstatePropertyId },
145 });
146
147 return response.data?.getRealEstateProperty;
148 },
149 });
150
151 // TanStack update mutation with optimistic updates
152 const updateMutation = useMutation({
153 mutationFn: async (realEstatePropertyDetails) => {
154 const response = await API.graphql({
155 query: mutations.updateRealEstateProperty,
156 variables: { input: realEstatePropertyDetails },
157 });
158
159 return response?.data?.updateRealEstateProperty;
160 },
161 // When mutate is called:
162 onMutate: async (newRealEstateProperty) => {
163 // Cancel any outgoing refetches
164 // (so they don't overwrite our optimistic update)
165 await queryClient.cancelQueries({
166 queryKey: ["realEstateProperties", newRealEstateProperty.id],
167 });
168
169 await queryClient.cancelQueries({
170 queryKey: ["realEstateProperties"],
171 });
172
173 // Snapshot the previous value
174 const previousRealEstateProperty = queryClient.getQueryData([
175 "realEstateProperties",
176 newRealEstateProperty.id,
177 ]);
178
179 // Optimistically update to the new value
180 if (previousRealEstateProperty) {
181 queryClient.setQueryData(
182 ["realEstateProperties", newRealEstateProperty.id],
183 /**
184 * `newRealEstateProperty` will at first only include updated values for
185 * the record. To avoid only rendering optimistic values for updated
186 * fields on the UI, include the previous values for all fields:
187 */
188 { ...previousRealEstateProperty, ...newRealEstateProperty }
189 );
190 }
191
192 // Return a context with the previous and new realEstateProperty
193 return { previousRealEstateProperty, newRealEstateProperty };
194 },
195 // If the mutation fails, use the context we returned above
196 onError: (err, newRealEstateProperty, context) => {
197 console.error("Error updating record:", err, newRealEstateProperty);
198 if (context?.previousRealEstateProperty) {
199 queryClient.setQueryData(
200 ["realEstateProperties", context.newRealEstateProperty.id],
201 context.previousRealEstateProperty
202 );
203 }
204 },
205 // Always refetch after error or success:
206 onSettled: (newRealEstateProperty) => {
207 if (newRealEstateProperty) {
208 queryClient.invalidateQueries({
209 queryKey: ["realEstateProperties", newRealEstateProperty.id],
210 });
211 queryClient.invalidateQueries({
212 queryKey: ["realEstateProperties"],
213 });
214 }
215 },
216 });
217
218 // TanStack delete mutation with optimistic updates
219 const deleteMutation = useMutation({
220 mutationFn: async (realEstatePropertyDetails) => {
221 const response = await API.graphql({
222 query: mutations.deleteRealEstateProperty,
223 variables: { input: realEstatePropertyDetails },
224 });
225
226 return response?.data?.deleteRealEstateProperty;
227 },
228 // When mutate is called:
229 onMutate: async (newRealEstateProperty) => {
230 // Cancel any outgoing refetches
231 // (so they don't overwrite our optimistic update)
232 await queryClient.cancelQueries({
233 queryKey: ["realEstateProperties", newRealEstateProperty.id],
234 });
235
236 await queryClient.cancelQueries({
237 queryKey: ["realEstateProperties"],
238 });
239
240 // Snapshot the previous value
241 const previousRealEstateProperty = queryClient.getQueryData([
242 "realEstateProperties",
243 newRealEstateProperty.id,
244 ]);
245
246 // Optimistically update to the new value
247 if (previousRealEstateProperty) {
248 queryClient.setQueryData(
249 ["realEstateProperties", newRealEstateProperty.id],
250 newRealEstateProperty
251 );
252 }
253
254 // Return a context with the previous and new realEstateProperty
255 return { previousRealEstateProperty, newRealEstateProperty };
256 },
257 // If the mutation fails, use the context we returned above
258 onError: (err, newRealEstateProperty, context) => {
259 console.error("Error deleting record:", err, newRealEstateProperty);
260 if (context?.previousRealEstateProperty) {
261 queryClient.setQueryData(
262 ["realEstateProperties", context.newRealEstateProperty.id],
263 context.previousRealEstateProperty
264 );
265 }
266 },
267 // Always refetch after error or success:
268 onSettled: (newRealEstateProperty) => {
269 if (newRealEstateProperty) {
270 queryClient.invalidateQueries({
271 queryKey: ["realEstateProperties", newRealEstateProperty.id],
272 });
273 queryClient.invalidateQueries({
274 queryKey: ["realEstateProperties"],
275 });
276 }
277 },
278 });
279
280 return (
281 <div style={styles.detailViewContainer}>
282 <h2>Real Estate Property Detail View</h2>
283 {isErrorQuery && <div>{"Problem loading Real Estate Property"}</div>}
284 {isLoading && (
285 <div style={styles.loadingIndicator}>
286 {"Loading Real Estate Property..."}
287 </div>
288 )}
289 {isSuccess && (
290 <div>
291 <p>{`Name: ${realEstateProperty?.name}`}</p>
292 <p>{`Address: ${realEstateProperty?.address}`}</p>
293 </div>
294 )}
295 {realEstateProperty && (
296 <div>
297 <div>
298 {updateMutation.isLoading ? (
299 "Updating Real Estate Property..."
300 ) : (
301 <>
302 {updateMutation.isError &&
303 updateMutation.error ? (
304 <div>An error occurred: {updateMutation.error.message}</div>
305 ) : null}
306
307 {updateMutation.isSuccess ? (
308 <div>Real Estate Property updated!</div>
309 ) : null}
310
311 <button
312 onClick={() =>
313 updateMutation.mutate({
314 id: realEstateProperty.id,
315 name: `Updated Home ${Date.now()}`,
316 })
317 }
318 >
319 Update Name
320 </button>
321 <button
322 onClick={() =>
323 updateMutation.mutate({
324 id: realEstateProperty.id,
325 address: `${Math.floor(
326 1000 + Math.random() * 9000
327 )} Main St`,
328 })
329 }
330 >
331 Update Address
332 </button>
333 </>
334 )}
335 </div>
336
337 <div>
338 {deleteMutation.isLoading ? (
339 "Deleting Real Estate Property..."
340 ) : (
341 <>
342 {deleteMutation.isError && deleteMutation.error ? (
343 <div>An error occurred: {deleteMutation.error.message}</div>
344 ) : null}
345
346 {deleteMutation.isSuccess ? (
347 <div>Real Estate Property deleted!</div>
348 ) : null}
349
350 <button
351 onClick={() =>
352 deleteMutation.mutate({
353 id: realEstateProperty.id,
354 })
355 }
356 >
357 Delete
358 </button>
359 </>
360 )}
361 </div>
362 </div>
363 )}
364 <button onClick={() => setCurrentRealEstatePropertyId(null)}>
365 Back
366 </button>
367 </div>
368 );
369 }
370
371 return (
372 <div>
373 {!currentRealEstatePropertyId && (
374 <div style={styles.appContainer}>
375 <h1>Real Estate Properties:</h1>
376 <div>
377 {createMutation.isLoading ? (
378 "Adding Real Estate Property..."
379 ) : (
380 <>
381 {createMutation.isError && createMutation.error ? (
382 <div>An error occurred: {createMutation.error.message}</div>
383 ) : null}
384
385 {createMutation.isSuccess ? (
386 <div>Real Estate Property added!</div>
387 ) : null}
388
389 <button
390 onClick={() => {
391 createMutation.mutate({
392 name: `New Home ${Date.now()}`,
393 address: `${Math.floor(
394 1000 + Math.random() * 9000
395 )} Main St`,
396 });
397 }}
398 >
399 Add RealEstateProperty
400 </button>
401 </>
402 )}
403 </div>
404 <ul style={styles.propertiesList}>
405 {isLoading && (
406 <div style={styles.loadingIndicator}>
407 {"Loading Real Estate Properties..."}
408 </div>
409 )}
410 {isErrorQuery && (
411 <div>{"Problem loading Real Estate Properties"}</div>
412 )}
413 {isSuccess &&
414 realEstateProperties?.map((realEstateProperty, idx) => {
415 if (!realEstateProperty) return null;
416 return (
417 <li
418 style={styles.listItem}
419 key={`${idx}-${realEstateProperty.id}`}
420 >
421 <p>{realEstateProperty.name}</p>
422 <button
423 style={styles.detailViewButton}
424 onClick={() =>
425 setCurrentRealEstatePropertyId(realEstateProperty.id)
426 }
427 >
428 Detail View
429 </button>
430 </li>
431 );
432 })}
433 </ul>
434 </div>
435 )}
436 {currentRealEstatePropertyId && <RealEstatePropertyDetailView />}
437 <GlobalLoadingIndicator />
438 </div>
439 );
440}
441
442export default App;
443
444const styles = {
445 appContainer: {
446 display: "flex",
447 flexDirection: "column",
448 alignItems: "center",
449 },
450 detailViewButton: { marginLeft: "1rem" },
451 detailViewContainer: { border: "1px solid black", padding: "3rem" },
452 globalLoadingIndicator: {
453 position: "fixed",
454 top: 0,
455 left: 0,
456 width: "100%",
457 height: "100%",
458 border: "4px solid blue",
459 pointerEvents: "none",
460 },
461 listItem: {
462 display: "flex",
463 justifyContent: "space-between",
464 border: "1px dotted grey",
465 padding: ".5rem",
466 margin: ".1rem",
467 },
468 loadingIndicator: {
469 border: "1px solid black",
470 padding: "1rem",
471 margin: "1rem",
472 },
473 propertiesList: {
474 display: "flex",
475 flexDirection: "column",
476 alignItems: "center",
477 justifyContent: "start",
478 width: "50%",
479 border: "1px solid black",
480 padding: "1rem",
481 listStyleType: "none",
482 },
483};