DEV Community

newAlicorn
newAlicorn

Posted on • Updated on

Building a CRUD SPA with Ruby on Rails and React

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
    }
}

Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)