visit
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.
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.
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 '//localhost:3006'
resource "*",
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
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: "//localhost:3000"
}
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
def post_params
params.require(:post).permit(:caption, :image)
end
[
{
"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
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": "//localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBFdz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--a216f6670f89e8f6d39fbce189af62e3d45633d6/jump%20rope.jpg"
},
{
"id": 15,
"caption": "Today's fashion",
"image": "//localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBGQT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--be43dde8cfaa13e097b95e936e1a9cfd817f21e3/fashion.png"
},
{
"id": 25,
"caption": "The sport I like",
"imageUrl": "//localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBIZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--31fcdcf03292d53f4d9df25c1a46dc12844992f5/jump%20rope.jpg"
}
]
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('//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('//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.