Front-End Web & Mobile

Building a social network app with Amplify Form Builder and Storage

For apps that are heavily image dependent, like social media apps, managing file upload and access is core to the app’s success. Integrating with all the services needed for cloud-based file management and access can be painful, but Amplify Studio can provide you with tools to integrate quickly and easily with AWS S3 storage.

In this post you will learn how to set up an Amplify project with Storage and Data, and use Amplify Studio’s Form Builder to build a basic social network feed app. Users can create posts with an image and those posts are shown in a feed.

Screen recording of a web app with file uploading and a social feed

Prerequisites

  • You should have the Amplify CLI installed. The easiest way is to install it globally with npm or yarn: npm i -g @aws-amplify/cli
  • An AWS account

Create an Amplify project

Go to the Amplify console and click New App > Build an app. We’ll use “amplify-formbuilder-storage,” but you can name your app whatever you like. After your app is setup, click on Launch Studio button.

For this project, we will need Authentication, Storage, and Data. Amplify Storage requires Authentication, so set that up first. Head to the Authentication tab and click Deploy. We’ll use the default configuration settings, but feel free to change the default settings. The deployment will take a minute or two to get everything set up.

Screenshot showing Amplify Studio for setting up Authentication

Once that is done, head to the Storage tab. Check the checkboxes for “upload” and “view” for “signed in users” and “guest users” and click Create bucket to deploy that as well.

Screenshot showing Amplify Studio setting up Storage

Finally, let’s set up a simple data model in the Data tab. Create a model called Post with body and image attributes that are strings and click Save and deploy.

Screenshot showing Amplify Studio data modeling

Setup frontend code

We will use NextJS for this example, but this code will work with any other React bundler or meta-framework like Vite. Create a new NextJS project:

yarn create next-app amplify-formbuilder-storage

For this example I chose these options:

  • Typescript: Yes
  • ESLint: Yes
  • Tailwind: No
  • src/ directory: Yes
  • app/ directory: No
  • Default import alias

Open the newly created directory, amplify-formbuilder-storage, in your favorite IDE. Then add the dependencies needed

yarn add @aws-amplify/ui-react @aws-amplify/ui-react-storage aws-amplify

Now pull your Amplify project into your NextJS codebase. Head to Amplify Studio and copy the command for local setup instructions, which should be a command that looks like amplify pull --appId [app id] --envName [env name], and paste that into your terminal. That command will set up all the necessary files in your application to connect to your Amplify backend.

Screenshot showing the amplify pull command

Configure Amplify

Add these imports to the top of the src/pages/_app.tsx:

import '@aws-amplify/ui-react/styles.css';
import { Amplify } from 'aws-amplify';
import awsconfig from '../aws-exports';

Then after the imports, add this to configure Amplify:

Amplify.configure(awsconfig);

Your src/pages/_app.tsx file should look like this now:

import '@aws-amplify/ui-react/styles.css'
import { Amplify } from 'aws-amplify'
import awsconfig from '../aws-exports'
import type { AppProps } from 'next/app'

Amplify.configure(awsconfig);

export default function App({ Component, pageProps }: AppProps) {
  return (
    <Component {...pageProps} />
  )
}

Next, update the src/pages/_document.tsx to add the Inter font:

import { Html, Head, Main, NextScript } from 'next/document'

export default function Document() {
  return (
    <Html lang="en">
      <Head>
        <link
          rel="stylesheet"
          href="https://fonts.googleapis.com/css2?family=Inter:slnt,wght@-10..0,100..900&display=swap"
        />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}

Finally, update the src/pages/index.tsx to use the PostCreateForm which Amplify generates for you automatically:

import PostCreateForm from '@/ui-components/PostCreateForm'

export default function Home() {
  return (
    <>
      <PostCreateForm />
    </>
  )
}

With the NextJS dev server running your app should look like this:

Screenshot showing default PostCreateForm component

Update the default form

All data models you create in Amplify Studio get default form components for creating and updating data. When you run amplify pull, Amplify Studio will generate React components for these forms in your project. Right now, the image attribute is just a plain text field and not an image uploader. Let’s fix that.

Open up Amplify Studio and edit the PostCreateForm that Amplify creates by default. Click on UI Library > PostCreateForm > Configure

Screenshot showing Amplify Studio Form Builder interface

Click on the image text field and change they type to a StorageField. Change the File level access to Public and change the Accepted file types to image/*.

Screenshot showing Amplify Studio Form Builder Storage Field

Now go back to your IDE/terminal and run amplify pull from your project. This will pull down the updated PostCreateForm component. Once that is done, run yarn dev to start up the local development server again. You should now see the form with the StorageField that lets users upload an image to their post. You might be wondering, how do you make sure files uploaded don’t overwrite other files? The Storage field in Form Builder actually handles that for you automatically by using a hash of the file contents to create a unique filename!

Create a post list

To display new posts, we will use DataStore to render the posts as they come in. At the tope of the Home function in src/pages/index.tsx add a state variable with useState:

  const [alert, setAlert] = React.useState<string>();

Then add a useEffect hook to set up the DataStore subscription:

  React.useEffect(() => {
    const sub = DataStore.observeQuery(Post, Predicates.ALL, {
      sort: (item) => item.createdAt(SortDirection.DESCENDING)
    }).subscribe(snapshot => {
      const { items } = snapshot;
      setItems(items);
    });

    return () => { sub.unsubscribe() }
  }, [])

The items state will be updated whenever new posts get created. Now lets render the items state in a Collection. Replace the JSX in the return statement with this:

    <Flex direction="row" alignItems="flex-start" minHeight="100vh">
      <View flex="1" position="sticky" top="0" padding="large">
        <PostCreateForm />
      </View>
      <View
        flex="1"
        alignSelf="stretch"
        backgroundColor="background.tertiary"
        padding="xl"
      >
        <Collection
          type="list"
          direction="column"
          justifyContent="left"
          items={items}
        >
          {(item, index) => (
            <Card variation="elevated">
              <StorageImage accessLevel="public" imgKey={item.image ?? ''} alt="" />
              <Text>{item.body}</Text>
            </Card>
          )}
        </Collection>
      </View>
    </Flex>

StorageImage is a new connected component that renders images uploaded from Amplify. Now you should see a list of posts and new posts should show up automatically in the list.

You could also use Amplify Studio’s Figma sync to design the post card component in Figma and create a collection in Amplify Studio to display all the posts. You can read more about Figma sync and collections in the docs.

Extending the form in code

There are a few more tweaks we can make to the PostCreateForm in our React code to make it work a bit better. First, let’s add a message when a post is saved. Create another state variable:

  const [alert, setAlert] = React.useState<string>();

Then on the PostCreateForm add an onSuccess handler:

        <PostCreateForm onSuccess={() => {
          setAlert('Post saved!')
          setTimeout(() => {
            setAlert(undefined);
          }, 2000);
        }} />
        {alert ? (<Alert variation="success">{alert}</Alert>) : null}

This will show a success banner below the form when a post is saved. The last piece is to clear the file from the StorageManger when a post is saved. The StorageManager component comes with an imperative handle through a React ref to clear the files. You can learn more about refs in React in the React docs. Create a ref which we will add to the StorageManager component:

  const ref = React.useRef<{ clearFiles: () => void }>(null);

Next, attach that ref to the StorageManager component through a component override. All Amplify generated UI components (from Figma sync or FormBuilder) have an overrides prop which lets you override any prop of any nested component. You can place props, like ref directly on a nested component without having to decompose the component. Here we will add a ref to the StorageManger component and then call the clearFiles() imperative handle after the form has been submitted.

        <PostCreateForm overrides={{
          image: {
            ref
          }
        }} onSuccess={() => {
          ref?.current?.clearFiles()
          setAlert('Post saved!')
          setTimeout(() => {
            setAlert(undefined);
          }, 2000);
        }} />

Now your src/pages/index.tsx file should look like this:

import * as React from 'react';
import { Post } from '@/models';
import PostCreateForm from '@/ui-components/PostCreateForm';
import { DataStore, Predicates, SortDirection } from 'aws-amplify';
import {
  Alert,
  Card,
  Collection,
  Flex,
  Text,
  View,
} from '@aws-amplify/ui-react';
import { StorageImage } from '@aws-amplify/ui-react-storage';

export default function Home() {
  const [items, setItems] = React.useState<Array<Post>>([]);
  const [alert, setAlert] = React.useState<string>();
  const ref = React.useRef<{ clearFiles: () => void }>(null);

  React.useEffect(() => {
    const sub = DataStore.observeQuery(Post, Predicates.ALL, {
      sort: (item) => item.createdAt(SortDirection.DESCENDING),
    }).subscribe((snapshot) => {
      const { items } = snapshot;
      setItems(items);
    });

    return () => {
      sub.unsubscribe();
    };
  }, []);

  return (
    <Flex direction="row" alignItems="flex-start" minHeight="100vh">
      <View flex="1" position="sticky" top="0" padding="large">
        <PostCreateForm
          overrides={{
            image: {
              ref,
            },
          }}
          onSuccess={() => {
            setAlert('Post saved!');
            ref?.current?.clearFiles();
            setTimeout(() => {
              setAlert(undefined);
            }, 2000);
          }}
        />
        {alert ? <Alert variation="success">{alert}</Alert> : null}
      </View>
      <View
        flex="1"
        alignSelf="stretch"
        backgroundColor="background.tertiary"
        padding="xl"
      >
        <Collection
          type="list"
          direction="column"
          justifyContent="left"
          items={items}
        >
          {(item, index) => (
            <Card variation="elevated" key={index}>
              <StorageImage
                accessLevel="public"
                imgKey={item.image ?? ''}
                alt=""
              />
              <Text>{item.body}</Text>
            </Card>
          )}
        </Collection>
      </View>
    </Flex>
  );
}

Screen recording of a web app with file uploading and a social feed

If you want to grab all the code, it is available on Github: https://github.com/dbanksdesign/amplify-formbuilder-storage

Optional Next steps

Add authentication

Your Amplify project already has an authentication backend, you just need to add sign in/sign up pages. With the Authenticator component you can do that in 10 lines of code!

Customize the look and feel

All Amplify UI components can be styled with plain CSS or with a Theme object to match your brand.

Summary

In this post we created a NextJS app and added file uploading capabilities with the Amplify UI StorageManager component. To clean up your project, run amplify delete from the command line or delete the app from the AWS console.