Using Active Storage with Amazon S3 and React
Published: 2023-05-04 09:30:51

In this blog post, we'll explore how to use Active Storage with Amazon S3 to store image files and how to use React to display those images in a web application.

## Setting up Amazon S3

The first step is to create an Amazon S3 bucket to store your image files. To do this, log in to your Amazon Web Services (AWS) account and navigate to the S3 dashboard. From there, click on the "Create bucket" button and follow the prompts to create your bucket.

Once your bucket is created, you'll need to generate an access key and secret key for your AWS account. These credentials will be used by Rails to access your S3 bucket. To generate your credentials, navigate to the AWS Identity and Access Management (IAM) dashboard and create a new user. Make sure to give the user the appropriate permissions to access your S3 bucket.

Once your bucket is set up, you will also want to add the following permission object into the CORS (Cross-origin resource sharing) configuration in your bucket settings: 

```
[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "PUT",
            "POST",
            "DELETE"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": []
    }
]
```

## Setting up Active Storage

The next step is to set up Active Storage in your Rails application. To do this, first, add the following line to your Gemfile:

```
gem 'aws-sdk-s3'
```

This will install the necessary dependencies for using Active Storage with Amazon S3.

Next, run the following command to set up Active Storage:

```
rails active_storage:install
```

This will generate a migration that adds the necessary tables to your database.

Run the migration with the following command:

```
rails db:migrate
```

You will also need to uncomment Amazon storage code in your storage.yml file and fill in the appropriate data:

```
amazon:
  service: S3
  access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
  secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
  region: your-region
  bucket: your-bucket-name
```

Next, with this command you will be able to edit your credential files and include the access key and secret access key provided by AWS:

```
EDITOR="code --wait" rails credentials:edit
```

In your development.rb file in your environments, you will also need to change your config.active_storage.service to Amazon:

```
config.active_storage.service = :amazon
```

Finally, in your initializers, in your cors.rb file, uncomment out the necessary code, and change the origins to whatever your project requires. For the sake of this example, a wildcard will be included:

```
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end
```

Now that Active Storage is set up, we can start using it to store our image files.

## Uploading Files

To upload an image file, first, create a form in your React application that allows users to select a file to upload. Here's an example:

```
import React, { useState } from 'react';

function ImageUploadForm() {
  const [file, setFile] = useState(null);

  function handleChange(event) {
    setFile(event.target.files[0]);
  };

  function handleSubmit(event) {
    event.preventDefault();
    const formData = new FormData();
    formData.append('image', file);

    fetch('/images', {
      method: 'POST',
      body: formData,
    });

    if (response.ok) {
      console.log('Image uploaded successfully');
    } else {
      console.log('Error uploading image');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="file" onChange={handleChange} />
      <button type="submit">Upload Image</button>
    </form>
  );
}

export default ImageUploadForm;
```

This form will allow users to select an image file to upload. Note that images must be appended to a new FormData as the information can not be sent as JSON. 

Next, create a Rails controller action to handle the file upload. Here's an example:

```
class ImagesController < ApplicationController
  def create
    image = Image.new(image_params)
    if image.save
      render json: { image_url: url_for(image.image) }, status: :created
    else
      render json: image.errors, status: :unprocessable_entity
    end
  end

  private

  def image_params
    params.permit(:image)
  end
end
```

This controller action creates a new `Image` object and assigns it the uploaded file. The `image_params` method is used to permit only the `image` parameter to be passed to the `Image` model. The response includes the URL of the uploaded image, which we'll use in the React application to display the image.

Another way to access the image’s url is to define the image_url in the serializer. Here’s an example for that:

```
class ImageSerializer < ActiveModel::Serializer
  include Rails.application.routes.url_helpers
  
  attributes :id, :image_url

  def image_url
    if object.image.attached?
      url_for(object.image)
    else
      ""
    end
  end
end
```

Note that the line “include Rails.application.routes.url_helpers” must be included to have access to the url_for helper.

Next, add the following code to your `Image` model to use Active Storage to store the image file:

```
class Image < ApplicationRecord
  has_one_attached :image
end
```

## Displaying Files

Now that we have uploaded our images to S3 using Active Storage, we can display them in our React application. Here's an example of how to display an image in React:

```
import React from 'react';

function ImageDisplay({ imageUrl }) {
  return (
    <div>
      <img src={imageUrl} alt="Uploaded Image" />
    </div>
  );
}

export default ImageDisplay;
```

This component takes the URL of the image as a prop and displays it using an `img` element.

Finally, we can use our `ImageUploadForm` and `ImageDisplay` components together to create a complete image upload and display feature:

```
import React, { useState } from 'react';
import ImageUploadForm from './ImageUploadForm';
import ImageDisplay from './ImageDisplay';

function App() {
  const [imageUrl, setImageUrl] = useState(null);

  function handleImageUpload(formData) {
    fetch('/images', {
      method: 'POST',
      body: formData,
    });

    if (response.ok) {
      r.json().then((data) => setImageUrl(data.image_url));
    } else {
      console.log('Error uploading image');
    }
  };

  return (
    <div>
      <ImageUploadForm onImageUpload={handleImageUpload} />
      {imageUrl && <ImageDisplay imageUrl={imageUrl} />}
    </div>
  );
}

export default App;
```

This component creates an `ImageUploadForm` and an `ImageDisplay` component. When an image is uploaded, the `handleImageUpload` function is called, which sends a POST request to the Rails API and sets the `imageUrl` state to the URL of the uploaded image. The `ImageDisplay` component is only shown if `imageUrl` is not null.

And that's it! You should now have a fully functioning image upload and display feature in your React application using Active Storage with Amazon S3.