Building a CRUD SPA with Ruby on Rails and React
newAlicorn
Posted on January 22, 2022
For my React project, I built a simple CRUD SPA called the Eat Here React App. This App allows users to explore the most unusual restaurants around the world. Besides reading all restaurants, users can add new restaurants, or like and make reviews of any of them. Searching by name functionality is also implemented in the application. Here is the demo video.
Building a RESTful JSON API with Rails
In this project, I continued utilizing the Ruby on Rails framework for my backend API construction. I had two related models set up: a restaurant model that has_many
reviews, a review that belongs_to
a restaurant. I also defined a couple of before_validation
methods for data validation. Below is the basic flow of how I built out the Rails API step by step :
Step 1 - Create a new Rails API using the command line below. Don’t forget to add the API flag at the end.
rails new eat-here-rails-backend --api
Step 2 - Specify the attributes and datatypes of both models and utilize rails g resource command to create corresponding models, controllers, database migration tables, and routes.
rails g resource Restaurant name country image gif description address
rails g resource Review nickname comment restaurant:belongs_to
Step 3 - Define index, show, create, update, and destroy actions and serialize data in relative controllers’ actions.
Step 4 - Define necessary validation and helper methods in models’ files.
Step 5 - Don’t forget to install the CORS gem and enable the CORS file to allow our server to specify from what origins it will permit.
Here is my backend repo on GitHub.
Building the Frontend App with React using Hooks
Since this was my first React project, I spent a lot of time understanding a couple of core concepts before coding. For example, the difference between props & state, React lifecycle, virtual DOM, controlled components, Redux, and React-Redux. It helped me better structure and re-factor my codes.
During the coding process, I found the most challenging part was how to utilize react redux to read state from the store and how to update state by dispatching actions. The basic flow of using react redux is: (1) We build up our actions; (2) We dispatch the actions to the reducer; (3) The reducer returns our state.
1. Create a Store
When finishing the basic installation of all necessary dependencies, the first step is to set up the global state. Calling the createStore
method provided by redux will return us the store object. Since I also incorporated asynchronous requests in this application, I used redux thunk as the middleware to handle all the asynchronous actions.
import { createStore, applyMiddleware, compose } from 'redux';
import rootReducer from '../reducers/rootReducer';
import thunk from 'redux-thunk';
const store = createStore(rootReducer, compose(applyMiddleware(thunk), window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()))
export default store
2. Define Reducers
Reducers are functions that accept the previous state as the first argument and an action object as the second argument and return the newly updated state. Note that reducers do not mutate the state directly. They return an entirely new state to replace the old one. Whatever the reducer returns will be our current state; The default action returns the initial state. Since I had two reducers created in my application, I utilized the combineReducers()
function to delegate different pieces of state to each reducer.
import { combineReducers } from 'redux';
import restaurantsReducer from './restaurantsReducer';
import reviewsReducer from './reviewsReducer';
const rootReducer = combineReducers({
restaurants: restaurantsReducer,
reviews: reviewsReducer
})
export default rootReducer
Below contains the code snippet of my restaurantReducer
:
const initState = {
restaurants: [],
loading: false
}
const restaurantsReducer = (state = initState, action) => {
switch(action.type){
case "LOADING":
return {
...state,
loading: true
}
case "ADD_RESTAURANT":
return {
...state,
restaurants: [...state.restaurants, action.payload]
}
case "FETCH_RESTAURANTS":
return {
...state,
restaurants: [...state.restaurants, ...action.payload],
loading: false
}
case "UPDATE_RESTAURANT":
const idx = state.restaurants.findIndex((restaurant) => restaurant.id === action.payload.id)
const restaurant = action.payload
return {
...state,
restaurants: [...state.restaurants.slice(0, idx), restaurant, ...state.restaurants.slice(idx + 1) ]
}
default:
return state
}
}
export default restaurantsReducer
3. Define All Actions
An action is an object that has a type and a payload. We can imagine the payload as objects/data that we want to send to our reducer. Also, since I made fetch requests in my action creator, the thunk middleware enabled me to return functions from my action creators and pass dispatch
as an argument to the returned functions.
const baseUrl = "http://localhost:5000/restaurants"
export const addRestaurant = (restaurantObj) => {
return {
type: "ADD_RESTAURANT",
payload: restaurantObj
}
}
export const fetchRestaurants = () => {
return (dispatch) => {
dispatch({type: "LOADING"})
fetch(baseUrl)
.then(resp => resp.json())
.then(data => {
dispatch({
type: "FETCH_RESTAURANTS",
payload: data
})
})
}
}
export const createRestaurant = (restaurant) => {
return (dispatch) => {
const configObj = {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body:JSON.stringify({restaurant})
}
fetch(baseUrl, configObj)
.then(resp => resp.json())
.then(restaurant => dispatch(addRestaurant(restaurant)))
}
}
export const updateRestaurant = (newObject) => {
return {
type: "UPDATE_RESTAURANT",
payload: newObject
}
}
4. Read and Update State in Relative Components
Since I used react hooks in this project, I imported useSelector
hook to connect to the store, and imported useDispatch
and useEffect
hooks to read and update the state in the components.
Feel free to check my frontend repo on GitHub.
Further Thoughts
For further development, I want to add the user authentication system to this project. The challenge is how to implement the jwt auth in both Redux and Rails. I will start researching and coding this topic from this article.
Posted on January 22, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.