aws-amplify / amplify-js

A declarative JavaScript library for application development using cloud services.
https://docs.amplify.aws/lib/q/platform/js
Apache License 2.0
9.44k stars 2.13k forks source link

RFC: React Hooks for Amplify API #4235

Closed ericclemmons closed 5 years ago

ericclemmons commented 5 years ago

This issue is a Request For Comments (RFC). It is intended to elicit community feedback regarding a proposed change to the library. Please feel free to post comments or questions here.

Purpose

Simplify integration of the Amplify API category to accomplish common use-cases within modern React applications.

The Problem

Running asynchronous GraphQL queries can easily lead to memory leaks, setting state on unmounted components, and incorrectly catching/handling errors:

useEffect(() => {
    setIsLoading(true);

    API.graphql({
      authMode: owner ? 'AMAZON_COGNITO_USER_POOLS' : 'AWS_IAM',
      query: listAlbums,
      variables: {
        nextToken: pageToken
      }
    })
      .then(payload => {
        setNextToken(payload.data.listAlbums.nextToken);
        setAlbums(payload.data.listAlbums.items);
        setIsLoading(false);
      })
      .catch(payload => {
        throw new Error(payload.errors[0].message);
      });

}, [owner, pageToken]);

Additionally, API.graphql may require the user to pass authMode, whereas it would be preferably for it to work "out of the box" without explicitly listening to Hub events.

Proposed Solution

useQuery(query, variables?, setData?)

returns [res, runQuery].

useQuery turns API.graphql into a synchronous API for managing the fetch/loading/data/error life-cycle:

const [res, runQuery] = useQuery(listAlbums, { nextToken: pageToken });

if (res.loading) return <Loader />;

if (res.error) return (
  <button onClick={() => runQuery()}>Retry</Button>
);

return (
  <>
    <AlbumList albums={res.data.listAlbums.items} />

    <button onClick={() => setPageToken(res.data.listAlbums.nextToken}>
      Next Page
    </button> 
  </>
);

useMutation(query, setData?)

Returns [res, runMutation].

useMutation provides a handler for API.graphql that turns the asynchronous event into a synchronous API for managing the fetch/loading/data/error life-cycle:

const [res, runMutation] = useMutation(deleteAlbum);

if (res.error) throw res.error;
if (res.data) return <Redirect to="/" />

return (
  <button onClick={() => runMutation({
    input: {
      expectedVersion: album.version,
      id: album.id
    }
  )}>Delete Album</button>
);

useSubscription(query, variables?, setData?)

returns undefined.

useSubscription pairs naturally with useQuery and useMutation for keeping state in sync, especially when mutations happen in other components:

const [album, setAlbum] = useState();

const [res] = useQuery(getAlbum, { id }, (data) => {
  setAlbum(data.getAlbum);
});

useSubscription(onUpdateAlbum, { owner }, (data) => {
  setAlbum(data.onUpdateAlbum);
});

if (res.loading) return <Loader />

return <AlbumDetails album={album} />

useGraphQL(query, variables?)

returns [runQuery].

useQuery, useMutation, and useSubscription are all powered by useGraphQL, which memoizes API.graphql calls to reduce network connections and automatically handle authMode.

This isn't intended to be used directly, but is leveraged by the other hooks.

Related Issues

Resources

jkeys-ecg-nmsu commented 5 years ago

Nice proposal!

One question: does the below code not exactly simulate this functionality asynchronously by having a local useState hook that will hold your API data and give it an initial state of undefined? Or is there something I'm missing about React lifecycles?

const [myApiData, setMyApiData] = useState(undefined);

useEffect( () => {
   const _effect = async () => await API.graphql(props.apiConfig);
   let data = _effect();
   setMyApiData(data);

  return () => setMyApiData(undefined); //set back to undefined so the lifecycle restarts on unmount and remount
}, []);

if(myApiData === undefined) return <Loading />
else if(myApiData.data.errors.length !== 0) return <Error />
else return <div>{myApiData.map( datum => <ul>{datum}</ul>}</div>

Then set it to undefined on unmount. The most annoying part with this pattern is that you have to wrap your async call to API.graphql in an async IIFE / closure inside your useEffect hook (which is then called synchronously).

All that being said, I am sure we will convert to using this just to reduce cruft.

3nvi commented 5 years ago

Thanks for that! It's nice how most GraphQL libraries have an aligned API in the use of hooks.

A few things I'm wondering:

  1. Doesn't the callback on useQuery & useMutation create the same memory-leak issues? If it executes right after the promise has resolved, I feel it might inevitably lead to the same problems if the component has unmounted while the request is in-flight.

  2. How is query memoization handled? Is the user allowed to choose among different cache & network strategies?

  3. Does the query get re-executed on a variable change or what's the plan to handle this scenario?

  4. I feel that an option to make the query "lazy" (behave like way mutation does) would be interesting whenever the query is triggered by some user action (i.e. button click).

ericclemmons commented 5 years ago

@3nvi Great feedback!

  1. Potentially, but in a reference implementation the mount status is tracked to prevent firing callbacks as the GraphQL Promise resolves. (There's an opportunity for AbortController to be used internally)

  2. The reference implementation memoizes based on query & variables (and potentially on auth state, based on how authMode is handled) via useMemo. At the very least the same as https://aws-amplify.github.io/docs/js/api#amplify-graphql-client for starters.

  3. The useQuery implementation would re-fire as variables change.

  4. Something like useLazyQuery would work, which is similar to how useMutation currently behaves: https://www.apollographql.com/docs/react/api/react-hooks/#uselazyquery

ajhool commented 5 years ago

According to the docs, the amplify api client is a lightweight alternative to apollo. Out of curiosity, in what way is this api client a lightweight alternative? It appears to have less features (apollo has ssr, hooks, caching, multiple languages, good documentation, etc.), has a much larger dependency footprint (gigantic), and doesn't offer codegen for custom queries/mutations. Configuration might be 10 LOC quicker.

Amplify feels overextended and my sincere RFC feedback for adding api hooks is to just add a page of documentation explaining how to point apollo at amplify's generated backend and drop support for this bespoke graphql client to focus on the more novel components of the amplify client

ericclemmons commented 5 years ago

@ajhool Thanks for your feedback! Your concerns are totally valid: Amplify covers a lot of ground (e.g. Analytics, API, Authentication, etc.), so over-extending and providing immature or unsupported APIs isn't an option.

The wording in the docs could be improved, but we offer Amplify as a "lightweight alternative" to the AWS SDK Client:

The AWS AppSync SDK enables you to integrate your app with the AWS AppSync service and integrates with the Apollo client found here. The SDK supports multiple authorization models, handles subscription handshake protocols for real-time updates to data, and has built-in capabilities for offline support that makes it easy to integrate into your app.

The Amplify GraphQL client is a lighter weight option if you’re looking for a simple way to leverage GraphQL features and do not need the offline capabilities or caching of the Apollo client. If you need those features, choose the AWS AppSync SDK.

https://aws-amplify.github.io/docs/js/api#using-aws-appsync

The ideal result of the hooks RFC is identifying common use-cases from you and others have, then iron out that friction in the underlying, framework-agnostic APIs.

A good example of identifying use-cases is your suggestion here: https://github.com/aws-amplify/amplify-js/issues/4325

Keep up the feedback and examples! They're essential to prioritize our efforts & make your experiences better.

ajhool commented 5 years ago

Thanks for the response. Two of the feature requests that I have open with Amplify are #4325 (the one you linked, a convenience function for making private files public) and #1910 (not mine, but I would have opened it if it wasn't already -- add cloudfront to the Storage module). Both of these use-cases are examples of where amplify is most useful for us; implementing common AWS-specific access patterns that Amplify is uniquely suited to make convenient for users. They make it easier for us to spend money on AWS infrastructure and add features to our own services.

The fact that the Storage module does not already have Cloudfront integration is precisely why it seems wasteful for the Amplify team to be reinventing/cloning-but-a-few-months-slower a graphql client. While the Storage module is not directly related to the API module, presumably time spent working on a Graphql API client is time not spent working on Storage or Auth or one of the more AWS-specific tools.

Pointing out a "good example" of a use-case that I've provided feels a littleee like it's dismissing my criticism here as being "not a good example." On the contrary, the feedback I'm trying to provide is that there is simply no use-case for me, an Amplify user, to use API hooks because we've already been using graphql hooks + ssr + cache + polling + other features for a few months via apollo-react.

The wording in the docs could be improved, but we offer Amplify as a "lightweight alternative" to the AWS SDK Client: https://github.com/awslabs/aws-mobile-appsync-sdk-js/

It's still unclear what "llightweight alternative" means in that context. Smaller footprint? Less features? It looks like the Appsync team has the right idea; that client + apollo integration documentation is already perfect and everything else about the API module is a distraction from that implementation IMO. Might solve this issue, too: https://github.com/aws-amplify/amplify-js/issues/3365#issuecomment-502009495

ericclemmons commented 5 years ago

That was my mistake. Your feedback has been great! What I didn't understand was if the hooks RFC was potentially redundant or conflicting with your needs.

And yes, your understanding of "lightweight alternative" is correct: smaller footprint, fewer features, fewer dependencies.

there is simply no use-case for me, an Amplify user, to use API hooks because we've already been using graphql hooks + ssr + cache + polling + other features for a few months. ... It looks like the Appsync team has the right idea; that client + apollo integration documentation is already perfect and everything else about the API module is a distraction from that implementation IMO.

This is fantastic feedback! Thanks so much for your thoughtful responses @ajhool.

jkeys-ecg-nmsu commented 5 years ago

Pointing out a "good example" of a use-case that I've provided feels a littleee like it's dismissing my criticism here as being "not a good example."

I tried to point out in a polite way that, unless I and most working React developers are missing something about lifecycles and useEffect, it was a proposed solution in search of a problem, and then got ignored. Feelsbadman. Maybe I need to be more direct when telling someone that I don't see the benefit of their proposed solution so that my criticism won't be similarly dismissed. @ericclemmons

I'd much rather see Amplify developers spend their efforts locking the authorization rules down for the API category than make it mildly easier to actually use the Amplify API SDK, which as @ajhool points out is redundant in the ecosystem.

ajhool commented 5 years ago

@jkeys-ecg-nmsu The apollo hooks implementation provides a loading, data, and error return for the hooks, so you can write very clean code to handle the three main states (If you have 3 different queries or mutations in a component, then things can get ugly quickly with the old HOC code but it is very clean with hooks).

React hooks, themselves, are so nice that they do make these implementations somewhat trivial, but the graphql clients do add some useful functionality over your basic useEffect and I recommend trying them out or adding it to your code!

Under the hood, those hooks are basically doing what you've described with useEffect + some convenience parameters and caching (https://github.com/apollographql/react-apollo/blob/master/packages/hooks/src/utils/useBaseQuery.ts), but I do find the react-apollo implementation to be a breath of fresh air after the previous HOC components strategy (both amplify's and apollo's, before apollo added hooks).

ericclemmons commented 5 years ago

Thanks everyone who's participated in this RFC, both here & on Twitter!

While we continue planning out our roadmap, I'd recommend anyone interested in hooks review the Resources in the original post body, which has existing, community-driven solutions for integrating Amplify within your existing React projects using hooks. 🙏

jkeys-ecg-nmsu commented 5 years ago

@ajhool thanks for the advice, I'll take a look at it. FWIW I specifically don't want caching and server-side rendering is not an option. So I use the API sdk since it's already a dependency and meets our needs. It also does the sigv4 signing to be fair which does free up some developer resources.

I absolutely agree that the Amplify team should be coding for the common Amazon use case -- if Azure or GCP want to leverage Amplify, they can submit pull requests and build plugins to implement boilerplate logic for their common use cases. I appreciate the provider-agnostic design, but when it comes to the actual Amplify SDK I want it to solve AWS problems first.

Maybe one feature request that branches from this conversation is the Amplify team declaring itself opinionated on things like recommended GraphQL clients, backend methods for querying AppSync* that don't require hacks to make Amplify work in a Lambda, etc. In general, any JS/TS software that is already popular in the ecosystem and solves a use case in a more robust manner than Amplify currently does or would, while still remaining unopinionated on application architecture. Then compile the list of recommended software / packages into a RFC and see what the community thinks about it. @ericclemmons could you please consider this (or consider, considering?) this in your roadmap planning?

*It's a shame that the AppSync documentation is so focused on how to create the API that it doesn't even mention how to access it, nor provide an SDK to communicate with it from the backend. You're just given an endpoint and trial-and-error. So then you have developers turning to Amplify and asking questions about how to use Amplify from Lambda, how to import just the API category to keep Lambda size small, etc. When really those questions should already be documented by the service team.

Even just a blurb on the AppSync page that describes how to use a client like axios would have probably prevented a lot of issues from being opened on this page.

amcdnl commented 4 years ago

I'd curious why not just use apollo hooks? I setup my project using apollo and appysync client and it works great (except for the tremendous amount of effort it took to figure it out).

Here is my appsync setup:

import Amplify from 'aws-amplify';
import AWSAppSyncClient, { createAppSyncLink } from 'aws-appsync';
import { ApolloLink } from 'apollo-link';
import { AWS_CUSTOM_CONFIG } from '../../aws-custom-exports';
import { errorLink } from 'core/Apollo';
import { jsonLink } from 'core/Apollo';
import { getToken } from 'core/Auth';

Amplify.configure(AWS_CUSTOM_CONFIG);

const auth = {
  type: AWS_CUSTOM_CONFIG.aws_appsync_authenticationType as any,
  jwtToken: () => getToken()
};

export const appSyncClient = new AWSAppSyncClient(
  {
    url: AWS_CUSTOM_CONFIG.aws_appsync_graphqlEndpoint,
    disableOffline: true,
    region: AWS_CUSTOM_CONFIG.aws_appsync_region,
    auth,
    cacheOptions: {
      addTypename: false
    }
  },
  {
    link: ApolloLink.from([
      jsonLink,
      errorLink,
      createAppSyncLink({
        url: AWS_CUSTOM_CONFIG.aws_appsync_graphqlEndpoint,
        region: AWS_CUSTOM_CONFIG.aws_appsync_region,
        auth,
        complexObjectsCredentials: () => Amplify.Auth.currentCredentials()
      })
    ])
  }
);

I had to hack the rehydrate like this:

import { useEffect, useState } from 'react';
import { useApolloClient } from 'react-apollo';
import AWSAppSyncClient from 'aws-appsync';

export const Rehydrated = ({ children }) => {
  const client = useApolloClient();
  const [rehydrated, setState] = useState(false);

  useEffect(() => {
    if (client instanceof AWSAppSyncClient) {
      (async () => {
        await client.hydrated();
        setState(true);
      })();
    }
  }, [client]);

  return rehydrated ? children : null;
};

but now that is all working I can do my queries just like apollo, example:

export const ConnectionsContainer: FC<ConnectionsContainerProps> = props => {
  const { data, loading } = useQuery(CONNECTION_QUERY);

  const [deleteConnection] = useMutation(
    DELETE_CONNECTION_MUTATION,
    mutationOptions
  );

and take advantage of the middlewares like error link:

import { onError } from 'apollo-link-error';

export const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path }) => {
      if (message === 'Valid authorization header not provided.') {
        window.location.replace('/login');
      }

      console.error(
        `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
      );
    });
  }

  if (networkError) {
    console.error(`[Network error]: ${networkError}`);
  }
});
ericclemmons commented 4 years ago

This RFC focused on simplifying existing usage patterns for the Amplify's GraphQL API within the React community, while preventing common bugs (forgetting to unsubscribe to subscriptions, setting state after unmounting, etc.).

Not all apps use react-apollo, but for those that do can re-use Apollo Client's hooks as you've done here!

amcdnl commented 4 years ago

@ericclemmons - It would be awesome if we could improve the docs to talk about this and maybe show how to do it easier.

mdegrees commented 4 years ago

@amcdnl, to @ericclemmons point, I went, for e.g, the route of react-apollo but hit a wall with multi authentications. My app have guests and users. Switching between Cognito pool and API key was cumbersome + subscriptions didn't work: https://github.com/awslabs/aws-mobile-appsync-sdk-js/issues/562

lenarmazitov commented 4 years ago

I'd curious why not just use apollo hooks? I setup my project using apollo and appysync client and it works great (except for the tremendous amount of effort it took to figure it out).

Here is my appsync setup:

import Amplify from 'aws-amplify';
import AWSAppSyncClient, { createAppSyncLink } from 'aws-appsync';
import { ApolloLink } from 'apollo-link';
import { AWS_CUSTOM_CONFIG } from '../../aws-custom-exports';
import { errorLink } from 'core/Apollo';
import { jsonLink } from 'core/Apollo';
import { getToken } from 'core/Auth';

Amplify.configure(AWS_CUSTOM_CONFIG);

const auth = {
  type: AWS_CUSTOM_CONFIG.aws_appsync_authenticationType as any,
  jwtToken: () => getToken()
};

export const appSyncClient = new AWSAppSyncClient(
  {
    url: AWS_CUSTOM_CONFIG.aws_appsync_graphqlEndpoint,
    disableOffline: true,
    region: AWS_CUSTOM_CONFIG.aws_appsync_region,
    auth,
    cacheOptions: {
      addTypename: false
    }
  },
  {
    link: ApolloLink.from([
      jsonLink,
      errorLink,
      createAppSyncLink({
        url: AWS_CUSTOM_CONFIG.aws_appsync_graphqlEndpoint,
        region: AWS_CUSTOM_CONFIG.aws_appsync_region,
        auth,
        complexObjectsCredentials: () => Amplify.Auth.currentCredentials()
      })
    ])
  }
);

I had to hack the rehydrate like this:

import { useEffect, useState } from 'react';
import { useApolloClient } from 'react-apollo';
import AWSAppSyncClient from 'aws-appsync';

export const Rehydrated = ({ children }) => {
  const client = useApolloClient();
  const [rehydrated, setState] = useState(false);

  useEffect(() => {
    if (client instanceof AWSAppSyncClient) {
      (async () => {
        await client.hydrated();
        setState(true);
      })();
    }
  }, [client]);

  return rehydrated ? children : null;
};

but now that is all working I can do my queries just like apollo, example:

export const ConnectionsContainer: FC<ConnectionsContainerProps> = props => {
  const { data, loading } = useQuery(CONNECTION_QUERY);

  const [deleteConnection] = useMutation(
    DELETE_CONNECTION_MUTATION,
    mutationOptions
  );

and take advantage of the middlewares like error link:

import { onError } from 'apollo-link-error';

export const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path }) => {
      if (message === 'Valid authorization header not provided.') {
        window.location.replace('/login');
      }

      console.error(
        `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
      );
    });
  }

  if (networkError) {
    console.error(`[Network error]: ${networkError}`);
  }
});

Thanks @amcdnl this works. But also there is need to add resolutions to package.json

  "resolutions": {
    "apollo-client": "2.6.8"
  }

Because AppSync uses apollo-client@2.4.6 which causes error see https://github.com/apollographql/react-apollo/issues/3148

amcdnl commented 4 years ago

@lenarmazitov I ended up changing the setup to this:

    "apollo-cache-inmemory": "^1.6.6",
    "apollo-client": "^2.6.10",
    "apollo-link": "^1.2.14",
    "apollo-link-error": "^1.1.13",
    "apollo-link-http": "^1.5.17",
    "apollo-link-ws": "^1.0.20",
    "aws-amplify": "^3.0.13",
    "aws-appsync-auth-link": "^2.0.2",
    "aws-appsync-subscription-link": "^2.1.0",

and

import Amplify from 'aws-amplify';
import { ApolloLink } from 'apollo-link';
import { AWS_CUSTOM_CONFIG } from '../../aws-custom-exports';
import { errorLink } from 'core/Apollo';
import { jsonLink } from 'core/Apollo';
import { getToken } from 'core/Auth';
import { createAuthLink } from 'aws-appsync-auth-link';
import { createSubscriptionHandshakeLink } from 'aws-appsync-subscription-link';
import ApolloClient from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { createHttpLink } from 'apollo-link-http';

Amplify.configure(AWS_CUSTOM_CONFIG);

const auth = {
  type: AWS_CUSTOM_CONFIG.aws_appsync_authenticationType as any,
  jwtToken: () => getToken()
};

const config = {
  url: AWS_CUSTOM_CONFIG.aws_appsync_graphqlEndpoint,
  region: AWS_CUSTOM_CONFIG.aws_appsync_region,
  auth,
  disableOffline: true
};

const cache = new InMemoryCache({ addTypename: false });
const httpLink = createHttpLink({ uri: config.url });

const link = ApolloLink.from([
  errorLink,
  jsonLink,
  createAuthLink(config),
  createSubscriptionHandshakeLink(config, httpLink)
]);

export const client = new ApolloClient({
  link,
  cache
});

to prevent the resolutions issue and just make things more simple.

lenarmazitov commented 4 years ago

@amcdnl why are you refused createAppSyncLink?

amcdnl commented 4 years ago

@lenarmazitov - See: https://github.com/awslabs/aws-mobile-appsync-sdk-js#using-authorization-and-subscription-links-with-apollo-client-no-offline-support

lenarmazitov commented 4 years ago

@amcdnl I tested both configs you provided

AppSyncClient works fine, I did not notice some troubles so far. ApolloClient: There is one problem with it, I lost ability to upload files via GraphQL like described here (of course it can uploaded manually with Storage module)

Did you have some troubles with AppSyncClient or why are you decided to use ApolloClient instead?

ChrisSargent commented 4 years ago

I think I am echoing most of the comments here when I say that, since I am already quite confused with a number of amplify vs appsync vs @aws-amplify vs aws-amplify imports vs AWS CDK etc etc.., I would prefer to use a defacto GraphQL client with an already established community and documentation (such as Apollo Client), and for the AWS team to document how to integrate it with the a GraphQL API built using Amplify. At the moment it feels like there is quite a lot of magic going on behind the scenes, which isn't always documented - great for going through tutorials and for when things 'just work' - but incredibly difficult to debug in the other 95% of usage.

And just to say that I do, so far, love using Amplify and to encourage keeping up the good work! :-)

carlosvega20 commented 4 years ago

Other way to achieve something similar with small footprint.

Query:

import { API, graphqlOperation } from 'aws-amplify'
import { listSomething } from 'graphql/query'
import {useAsync} from 'react-use'

const Demo = () => {
  const {loading, error, value} = useAsync(async () =>
    API.graphql(graphqlOperation(listSomething)) //await client.query({query: gql(listSomething)})
  , [])

  return (
    <div>
      {loading
        ? <div>Loading...</div>
        : error
          ? <div>Error: {error.message}</div>
          : <div>Value: {JSON.stringify(value)}</div>
      }
    </div>
  )
}

Mutation:

import { API, graphqlOperation } from 'aws-amplify'
import { createSomething } from 'graphql/mutations'
import {useAsyncFn} from 'react-use'

const Demo = () => {
  const [{loading, error, value}, mutateFn] = useAsyncFn(async (method, variables) =>
    API.graphql(graphqlOperation(method, variables)) //await client.mutate({mutation: gql(method), variables})
  , [])

  return (
    <div>
      {loading
        ? <div>Loading...</div>
        : error
          ? <div>Error: {error.message}</div>
          : <div>Value: {JSON.stringify(value)}</div>
      }
      <button onClick={() => mutateFn(createSomething, {input: {a: 10} })}>Create Something</button>
    </div>
  )
}
danigb commented 4 years ago

Following @carlosvega20 suggestion, this is the same with react-query (adds cache and other goodies)

Example in typescript👇

Query

import { API, graphqlOperation } from 'aws-amplify'
import { useQuery } from "react-query";
// those are generated by Amplify
import { GetPostQuery } from "../API";
import { getPost } from "../graphql/queries";

const { data: post, isLoading, refetch } = useQuery(
  ["post", { id: params.id }],
  async (_, { id }) => {
    const result: any = await API.graphql(graphqlOperation(getPost, { id }));
    return result.data as GetPostQuery;
  }
);

Mutation

import { API, graphqlOperation } from 'aws-amplify'
import { useMutation } from "react-query";
// those are generated by Amplify
import { DeletePostMutationVariables } from "../API";
import { deletePost } from "../graphql/mutations";

const [deleteGalleryMutation, { isLoading: isDeleting }] = useMutation(
  async (variables: DeletePostMutationVariables) =>
    await API.graphql(graphqlOperation(deletePost, variables))
);
amcdnl commented 4 years ago

Each of these approaches have some downsides, for instance:

github-actions[bot] commented 3 years ago

This issue has been automatically locked since there hasn't been any recent activity after it was closed. Please open a new issue for related bugs.

Looking for a help forum? We recommend joining the Amplify Community Discord server *-help channels or Discussions for those types of questions.