Rails API to React/Redux with Hooks

Will Carter
9 min readDec 3, 2020

--

Heroes and Villains

Rails => React + Hooks + Redux

This post will walk through a quick build of a Ruby on Rails API and a React/Redux front end. On the front end, we will add Redux to it to enable a global state, which can be accessed by all components within the App, regardless of nesting level. Redux thunk middleware will be used to help make asynchronous network requests and React Hooks will be used to dispatch actions to update and retrieve data from the Redux global store.

The Rails Back End

This tutorial assumes that you have rails installed. Check this Getting Started with Rails guide if you are new to Rails.

Create a new api with rails at the command line. Make the database postgresql and flip the api switch so that rails front end code related wont be generated (views).

$ rails new hero_api — database=postgresql — api

Create a user named, “hero_api”, to act as the user to connect to the database for this app. The database is also called hero_api in this case. Note password used for hero_api user here.

$ sudo adduser hero_api

Make hero_api user be a root user (optional):

$ sudo usermod -aG sudo hero_api

Impersonate the postgres user on your system and create the hero_api database and hero_api database user (not the same as the system user).

$ su — postgres
$ createuser hero_api
$ createdb hero_api

Now there is a database user called “hero_api” and a database called, “hero_api”. Next, provide the privileges to hero_api user within sql:

postgres@computer:~$ psql
postgres=# alter user hero_api with encrypted password '<password>';
postgres=# grant all privileges on database hero_api to hero_api;

Now the user, “hero_api”, has full privileges on the database, “hero_api”. Confusing, I know. Naming the user and the database with the same name seems to be convention.

Exit out of psql with ‘exit’, then $ exit to stop impersonating postgres. Next, edit config/database.yml to add credentials for the user, hero_api. Fill in the development areas to match the hero_api user above.

development:
adapter: postgresql
encoding: unicode
database: hero_api
pool: 5
username: hero_api
password: <password>

Scaffold two models, one for Hero and one for Villain. In our little universe, a hero has many villains and a villain has only one hero. Well give our heros and villains a name and img_url (link to an image photo of them). If all goes well, the db:migrate will create Hero and Villain models.

$ rails g scaffold Hero name:string img_url:string
$ rails g scaffold Villain name:string img_url:string hero:references
$ rake db:migrate

Next, we will create some heroes and villains in our universe with the rails console.

$ rails c
> b = Hero.new
> b.name = "Batman"
> b.img_url = "https://res.cloudinary.com/fergusdev/image/upload/v1608230102/heros_and_villains/batman_adam_west_vxpqdk.jpg"
> b.save
> j = Villain.new
> j.name = "Joker"
> j.img_url = "https://res.cloudinary.com/fergusdev/image/upload/v1608230367/heros_and_villains/joker_qbpxce.jpg"
> j.hero = b
> j.save

Don’t forget to set up the rack-cors middleware, so that API requests can come through easily. Add the following in /config/application.rb

/config/application.rbmodule HeroApi
...
class Application < Rails::Application
config.middleware.insert_before 0, Rack::Cors do
allow do
origins ‘*’
resource ‘*’, :headers => :any, :methods => [:get,
:post,
:patch,
:delete,
:options]
end
end
end

Now we have a REST API capable of CRUD operations on Heroes and Villains. Start up the service:

$ RAILS_ENV=development rails s

I went ahead and put the API up on Heroku, so it can be hit live. Here’s an example API call to get the hero with id = 1

Get hero number 1, Batman
Joker is but one of Batman’s many villains

So, the following routes are functional. Fire away and make some new heroes and villains in our universe.

GET heros           https://hero-api-56790.herokuapp.com/heros
GET villains https://hero-api-56790.herokuapp.com/villains
GET hero https://hero-api-56790.herokuapp.com/heros/1
Get villain https://hero-api-56790.herokuapp.com/villains/1
POST create hero https://hero-api-56790.herokuapp.com/heros
POST create villian https://hero-api-56790.herokuapp.com/villains

Want to spin up your own rails back end? The Github repository can be found at: https://github.com/FergusDevelopmentLLC/hero_api

The React/Redux Front End

The plan is to use the React library for the front end. We’ll include Redux to act as our front end global state. Also, we’ll use functional components with React hooks to initiate requests to our API populate populate the global state with data received from the API. The component will display data from the populated Redux store.

First off, start a totally new project in a new folder with create-react-app:

$ npx create-react-app heros_fe
$ cd heros_fe
$ npm install

Install the redux, react-redux, and redux thunk:

$ npm install redux react-redux redux-thunk

Redux will give our app the ability to have a global store or state that can be easily shared across components. Components can “select” from the Redux store or dispatch actions on it, which, when paired with a Reducer update the global state. Redux is useful for when an application becomes complex and many different parts of it needs to access and make updates to the global state. The Redux pattern also brings the asynchronous network calls to APIs to a single entry point in the code through actions and reducers. This makes network calls easier to maintain and less buggy. Redux is separate from React, which is why the react-redux package is also necessary.

The redux-thunk middleware gives the ability for actions to be asynchronous, as is the case here when calling an API. From their github repo:

“…the redux-thunk middleware extends the redux store’s abilities, and lets you write async logic that interacts with the store.”

Setup and boilerplate

Redux has a bit of boilerplate setup 😐, but once you get it going, it becomes more clear. In the /src folder create two sub-folders, actions and reducers:

/src/actions
/src/reducers

Inside of the actions folder, create a file called index.js with the following 3 constants.

/src/actions/index.js:export const FETCH_HEROS_REQUEST = 'FETCH_HEROS_REQUEST'
export const FETCH_HEROS_SUCCESS = 'FETCH_HEROS_SUCCESS'
export const FETCH_HEROS_FAILURE = 'FETCH_HEROS_FAILURE'

This is the set of action types that will be used to fetch the heros (misspelling intentional) from the API in the following order:

1. make the request (FETCH_HEROS_REQUEST)
2. request succeeds (FETCH_HEROS_SUCCESS)
3. request hits an error (FETCH_HEROS_FAILURE)

Think of actions like events that “reducers” respond to. So, let’s make the hero reducer.

Create a heroReducer.js file next in /src/reducers/heroReducer.js. This is where the updates to the Redux global store are made, depending on the action.type.

/src/reducers/heroReducer.js:import { 
FETCH_HEROS_REQUEST,
FETCH_HEROS_SUCCESS,
FETCH_HEROS_FAILURE } from “../actions/”
const initialState = {
heros: {},
isLoading: false
}
export default (state = initialState, action) => {
switch(action.type) {
case FETCH_HEROS_REQUEST:
return {
…state,
isLoading: true
}
case FETCH_HEROS_SUCCESS:
return {
…state,
isLoading: false,
heros: action.payload
}
case FETCH_HEROS_FAILURE:
return {
…state,
isLoading: false,
error: action.payload
}
default: . Make the contents of this action to be the following:
return state
}
}

First, notice that in in heroReducer.js the three action types are referenced: FETCH_HEROS_REQUEST, FETCH_HEROS_SUCCESS, and FETCH_HEROS_FAILURE. When each of these action types occur, a portion global state will be updated.

Second, a const object called initialState is created. This represents the initial state of heroReducer portion of the Redux global store when it is instantiated. Notice that the initial values for hero is {} (an empty object) and isLoading is set to false.

Next, the default function switches its return value based on the action.type being passed. With each type, a different portion of the Redux heroReducer global state is is updated.

For example, if a FETCH_HEROS_REQUEST is made, only the isLoading flag is switched to true in the global state, the rest of the state remains unchanged. This isLoading flag can be used to display a loading message or spinner to the user while the asynchronous communication is being made to the API.

Accordingly, if FETCH_HEROS_SUCCESS is the action.type, the loading flag is set to false and the heros store value is updated with the payload that is sent from the action along with the call (all the heros).

Finally, if an error occurs (FETCH_HEROS_FAILURE), the loading is set to false and the error property is populated with the error that came on the failed call.

Now, create the following file in /src/actions/heroActions. This file contains the action creator functions with respect to heros. The fetchHeros function here dispatches the three action.types mentioned above when the fetchHeros is called:

ed/src/actions/heroActions:import {
FETCH_HEROS_REQUEST,
FETCH_HEROS_SUCCESS,
FETCH_HEROS_FAILURE
} from './'
export const fetchHeros = () => {
return dispatch => {
dispatch({ type: FETCH_HEROS_REQUEST })fetch(`https://hero-api-56790.herokucalledapp.com/heros`, null)
.then(res => {
if(res.ok) {
return res.json()
}
else {
return Promise.reject(res.statusText)
}
})
.then((heros) => {
return dispatch({
type: FETCH_HEROS_SUCCESS,
payload: heros
})
})
.catch((error) => {
return dispatch({
type: FETCH_HEROS_FAILURE,
payload: error
})
})
}

Notice that in the fetchHeros function the 3 actions are dispatched in turn…

FETCH_HEROS_REQUEST
…then FETCH_HEROS_SUCCESS if there is a successful call
…then FETCH_HEROS_FAILURE if the call fails.

Notice that there is no payload on FETCH_HEROS_REQUEST, but the payload is heros if the type is FETCH_HEROS_SUCCESS and the payload is the error if the the type is FETCH_HEROS_FAILURE.

Next we have to do some more setup. Because the Redux store can be divided into many reducers, we need to combine them into a root reducer in /src/reducers/index.js

/src/reducers/index.js
import { combineReducers } from 'redux'
import heroReducer from './heroReducer'
const appReducer = combineReducers({
heroReducer
})
const rootReducer = (state, action) => {
if (action.type === ‘CLEAR_DATA’) {
state = undefined
}
return appReducer(state, action)
}
export default rootReducer

In a complex app, there will be more reducers or portions of the store than just one like we have here. /src/reducers/index.js is the place where they all are combined in order to create the Redux global state.

Finally, the rootReducer, the initalState, and the composed middlewares are used by the createStore function to create the store. Redux thunk is the middleware that allows for asynchronous requests. It uses a handy combineReducers function provided by Redux to combine different reducers and apply middlware. Here ‘redux-thunk’ middleware is applied when we combine all the reducers.

/src/store.js:import { createStore, applyMiddleware, compose } from ‘redux’
import thunk from 'redux-thunk'
import rootReducer from ‘./reducers’
const initialState = {}
const middleware = [thunk]
const store = createStore(
rootReducer,https://github.com/FergusDevelopmentLLC/pies_fe
initialState,
compose(
applyMiddleware(…middleware),
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__()
)
)
export default store

Next, the app needs to be aware of Redux. We do this with the Provider from the react-redux package and wrap the whole <App/> in a Provider in /src/index.js, passing it the store, created above.

/src/index.jsimport React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import { Provider } from 'react-redux'
import store from './store'
ReactDOM.render(
<React.StrictMode>
<Provider store={ store }>
<App />
</Provider>
</React.StrictMode>, document.getElementById(‘root’))

Now, we can make a hero presentation component in /src/components. All this component does is render HTML with the hero’s name and img_url based on a required hero prop sent to it.

/src/components/Hero.js:import React from 'react'
import PropTypes from 'prop-types'
const Hero = ({https://github.com/FergusDevelopmentLLC/pies_fe hero }) => {return (
<div className=”hero”>
{ hero.name }<br/>
<img src={ hero.img_url } width={100} />
</div>
)
}Hero.propTypes = {
hero: PropTypes.object.isRequired
}
export default Hero

Next, update App.js to the following:

/src/App.jsimport React, { useEffect } from 'react'
import { fetchHeros } from './actions/heroActions'
import { useDispatch, useSelector } from 'react-redux'
import Hero from './components/Hero'
const App = () => { const dispatch = useDispatch()
const heros = useSelector(state => state.heroReducer.heros)
useEffect(() => {
dispatch(fetchHeros())
}, [])
const getHeros = () => {
if(heros) {
return heros.map((hero) => <Hero hero={ hero } />)
}
else {
return "loading..."
}
}
return (
<div className='App'>
{ getHeros() }
</div>
)
}
export default App

First, notice that the fetchHeros function is imported from heroActions.

The useEffect hook is used to dispatch the fetchHeros function, which in turn dispatches a FETCH_HEROS_REQUEST action type and a FETCH_HEROS_SUCCESS or FETCH_HEROS_FAILURE depending on whether the request is successful or note. The useDispatch hook is imported from from react-redux to be able to dispatch(fetchHeros()) inside the useEffect callback.

Similarly, the useSelector hook is used to assign a value from the Redux store to the const, heros. The useSelector hook returns a function that contains the current state of the Redux store. The heros const will be populated with the array of heros from the API when the FETCH_HEROS_SUCCESS action.type occurs. The array of heros from the payload of this action assigned to an array of heris then mapped to a collection of <Hero /> components and displayed within App.js. Here is our list of heros, from Postgres to Redux.

The source for the front end can be found at: https://github.com/FergusDevelopmentLLC/heros_fe

Heroes!

--

--