How to Upload an Image From React to the Rails API Using React-hook-form and Context API

Written by Habibu | Published 2022/11/10
Tech Story Tags: ruby-on-rails | reactjs | context-api | rails-api | react-hook-form | front-end-development | programming | guide

TLDRAfter reading this article, you should always be able to create a React UI that allows you to use react-hook-form to upload an image to an API endpoint. It will also address the problems that arise when the conventions are not followed. This article is intended for readers who are already familiar with the following topics. We must create the **Rails** API-only project by following the steps shown below. To enable image uploading, we must first install active storage by running the command below.via the TL;DR App

After reading this article, you should be able to create a React UI that allows you to use react-hook-form to upload an image to an API endpoint. It will also address the problems that arise when the conventions are not followed.

Prerequisite

This article is intended for readers who are already familiar with the following topics.

  • making a Rails API-only application
  • with basic knowledge of React

Rails part

So, first and foremost, we must create the Rails API-only project by following the steps

shown below, type the command to create a new rails app with the default of --api and PostgreSQL as the default database.

rails new myprojectname -d postgresql --api

Navigate to the project folder and run bundle install

Since we are using PostgreSQL as our database. Let us configure the database by going to the following path config/database.yml then set the host, username, and password as in the example below

default: &default
  adapter: postgresql
  encoding: unicode
  host: localhost
  username: postgres
  password: myPassword123

Run rails db:create to create the database.

Our PostgreSQL database is now complete.

It's now time to set up the cors so that other clients can use our API. Let's open the gem file and add the rack-cors gem as shown below.

gem "rack-cors"

and then run bundle install

Then, in config/initializers/cors.rb, set the origins to your client URL, as shown below.

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    # origins "www.clienturl.com"
    origins 'http://localhost:3006'
    resource "*",
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

since only the local environment will be covered in this article.

Let's make the development environment's URL the default. Navigate to config/environments/development.rb and write the code below.

Rails.application.routes.default_url_options = {
    host: "http://localhost:3000"
}

Let's start with the Post model, which I'll make using a scaffold generator by typing the command below.

rails g scaffold Post caption:string

then run the migration

rails db:migrate

Our REST API is now complete, and we can make API calls from the terminal, but we are unable to upload an image. To enable image uploading, we must first install active storage by running the command below.

bin/rails active_storage:install
rails db:migrate

Now, using the path app/models/post.rb, navigate to the post model and add the image attachment code as shown below.

class Post < ApplicationRecord
    has_one_attached :image

end

You can also add model validation by including the code below.

 validates :caption, :image, presence: true

Then, in the posts controller, permit the image attribute as shown below

 def post_params
    params.require(:post).permit(:caption, :image)
 end

Now, the API returns data in JSON format, as shown below.

[
  {
    "id": 14,
    "caption": "Jumping rope time",
    "created_at": "2022-10-31T06:00:32.614Z",
    "updated_at": "2022-10-31T06:00:32.645Z"
  },
  {
    "id": 15,
    "caption": "Today's fashion",
    "created_at": "2022-10-31T06:00:50.969Z",
    "updated_at": "2022-10-31T06:00:50.997Z"
  }
]

The data above are attributes returned from our REST API response; we see id, caption, created_at, and updated_at, but no image attribute. We need a way to present our data using serializers or representers to fix this. You can use an active model serializer gem, but I won't go into detail about it in this article. I'm going to create my representers, which will determine which attributes are returned as JSON by restricting and allowing some of the data, for example, you may choose to hide the date-time attributes while displaying the image URL.

Because our data is coming from the index action in posts_controller.rb, the method looks like this:

def index
  @posts = Post.all
  render json: @posts
end

We need to replace the above code with

def index
   @posts = Post.all 
   render json: PostRepresenter.new(@posts).as_json
end

Since the PostRepresenter class is not defined, as you can see above, we must first open our app folder, then create the representers folder, and finally, create a file called posts_representers.rb and add the following code to it.

class PostsRepresenter
    def initialize(posts)
      @posts = posts
    end
  
    def as_json
      posts.map do |post|
        {
          id: post.id,
          caption: post.caption,
          image: post.imageUrl
        }
      end
    end
  
    private
  
    attr_reader :posts
  end

The PostsRepresenter class, which is represented by the code above, has a method called as_json. It uses a map block method to repeatedly iterate through all posts and displays the information in JSON format.

As you can see, we displayed the image attribute image: post.imageUrl but the imageUrl is not defined, so let us define it in the app/model/post.rb file.

 def imageUrl   
    Rails.application.routes.url_helpers.url_for(image) if image.attached?
 end

As a result, our Post model will look like this:

class Post < ApplicationRecord
    has_one_attached :image
    validates :caption, :image, presence: true

    def imageUrl   
      Rails.application.routes.url_helpers.url_for(image) if image.attached?
    end
end

Our REST API is now complete, and anyone can consume it using any front-end application such as React, Vue, or others, and it returns the JSON response shown below.

[
  {
    "id": 14,
    "caption": "Jumping rope time",
    "image": "http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBFdz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--a216f6670f89e8f6d39fbce189af62e3d45633d6/jump%20rope.jpg"
  },
  {
    "id": 15,
    "caption": "Today's fashion",
    "image": "http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBGQT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--be43dde8cfaa13e097b95e936e1a9cfd817f21e3/fashion.png"
  },
  {
    "id": 25,
    "caption": "The sport I like",
    "imageUrl": "http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBIZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--31fcdcf03292d53f4d9df25c1a46dc12844992f5/jump%20rope.jpg"
  }
]

React.js part

This section will go over the Axios for making API calls, as well as the react-hook-form and the context API. Now, run the following command to create the react app.

npx create-react-app imageuploader

cd imageuploader

Since the rails API is running on port 3000. Let us change the front-end port to 3006 by creating a .env file and pasting the code below.

PORT=3006

then, by entering the following commands, we'll install the dependencies we'll need for this project.

npm install axios
npm install react-hook-form 

Since AddPost.js and DisplayPost.js are currently empty, we must first create them before importing them into App.js. We are aware that we must use the context API to manage states. To use it throughout all of our components, we will create the context PostContext. After that, we will apply this context to the returned jsx. Any app component will be able to access the state.

import { createContext, useState } from "react";
import AddPost from "./components/AddPost";
import DisplayPost from "./components/DisplayPost";

export const PostContext = createContext(null); // Defining the context

function App() {
  const [post, setPost] = useState(PostContext) // Defining the states using context
  return (
    <PostContext.Provider value={{post, setPost}}> // Wrapping the app with the context API
      <div className="container">
       <AddPost />
       <DisplayPost />
      </div>
    </PostContext.Provider>
  );
}

export default App;

We must now focus on the AddPost.js component. Let us create a form that, when submitted, will send a POST request to the API and store the response in the state. To use the react-hook-form, we must first import the useForm hook. This hook has a handleSubmit method that receives a callback sendDataToApi that receives form inputs. To upload an image, we must use FormData() object and append the form inputs. It is common practice to use the string containing the rails “modelname[attribute]”. If the name of your model is Post and it has an attribute like image, you could use "post[image]" as the key to the Formdata; otherwise, the rails backend app will throw an error saying Unpermitted parameter::image. When the post request is successful, you must save the response data's state to the context API.

import React, { useContext, useState } from 'react'
import axios from 'axios';
import { useForm } from 'react-hook-form';
import { PostContext } from '../App';

const AddPost = () => {
  const { register, handleSubmit, reset } = useForm(); // 
  const {post, setPost} = useContext(PostContext);

  const sendDataToApi = (data) => {
    const formData = new FormData()
    const post = { ...data, image: data.image[0] }
    formData.append('post[caption]', post.caption)
    formData.append('post[image]', post.image)
    console.log(formData)
    axios.post('http://localhost:3000/posts', formData, {
      headers: { 'Content-Type': 'multipart/form-data' },
      withCredentials: true,
    })
    .then((response) => {
      if(response.data.status === 'created') {
        setPost(response.data.post)
      }
    })
    reset()
  };

 return (
    <div>
      <h1>Wall of fame</h1>
      
      <form className="form" onSubmit={handleSubmit(sendDataToApi)}>
        <div className="form-floating mb-2 col-10">
          <input type="file"  name="image"  {...register('image')} accept="image/*" />
          <label htmlFor="floatingInputImage">Image</label>
        </div>
        <div className="form-floating mb-2 col-10">
          <input type="file"  name="caption"  {...register('caption')} accept="image/*" />
          <label htmlFor="floatingInput">Caption</label>
        </div>
                  
        <div className="form-floating mb-3 col-10">
           <button type="submit" className="btn btn-primary ">Add Car</button>
        </div>
      </form>
    </div>
  )
}

export default AddPost

Let us go to DisplayPost.js, here on page refresh useEffect hook will fetch all posts using the get request and save them to the context API using setPost method. See the code below it explain it well.

import React, { useContext, useEffect } from 'react'
import axios from 'axios'
import { AppContext } from '../App'
import Loading from './Loading';


const DisplayPost = () => {
    const {post, setPost} = useContext(AppContext);
    
    useEffect(() => {
        axios.get('http://localhost:3000/posts')
        .then((response) => {
        setPost(response.data)
        })
    }, [setPost])

  
    
  return (
    <div className='container border border-info'>
        <div className="row">
            {
                Array.from(post).map((data) => {
                    return (
                        <div className="col-3"  key = {data.id}>
                            <div className="card">
                                <img className="card-img-top" src={data.image} alt="url for foto" />
                                <div className="card-body">
                                  <p className="card-text">{data.caption}</p>
                                </div>
                            </div>
                        </div>
                    )
                })
            }
        </div>
    </div>
  )
}

export default DisplayPost

Now that the front end is complete, you can upload the image to the Ruby on Rails API backend locally. To make it work in production, we must store the photos in services such as AWS S3 buckets, which I will discuss in the following article.

Thank you for your time.


Written by Habibu | Full-stack software developer. Ruby | Ruby on Rails | Javascript | React | Redux | React-native | Typescript
Published by HackerNoon on 2022/11/10