Creating a React Native App With a Ruby On Rails Backend (Part 2 of 3)

Written by Armaiz Adenwala

In the previous article, we set up the Ruby on Rails API. Now, we are ready to set up our React Native app and implement a state management system. For this tutorial, we will use the 0.60.5 version of React Native and Redux.


Note: Don’t forget to keep the Ruby on Rails API running in a separate terminal tab so you can test your connection throughout the tutorial.


Setting Up a React Native App


The process for setting up your React Native app will vary depending on your computer’s operating system and the platform you choose for rendering the app.


First, run cd .. to go to the project’s root directory. Then, go to React Native’s Getting Started guide. Click on the React Native CLI Quickstart tab, and follow the instructions. Make sure you are viewing instructions for the OS (MacOS, Windows, or Linux) and platform (Android or iOS) combination you will be using to render the app.


When you arrive at the Creating a new application section, do not install the most recent version of React Native. Instead, install the 0.60.5 version using this command:

    
react-native init noteApp --version [email protected]
    
  

Continue following the steps in the React Native guide until you see the Welcome Screen on your emulator or device.


Installing Redux


Redux manages app-wide state in a single store object that is passed to specific components, usually called connected components. The data passed in from the store object is then passed down to lower level components as props from connected components. Actions, with ActionTypes, are dispatched to Reducers that respond by updating the state of the app. To learn more about Redux, take a look at the Redux docs.


We also need to install redux-thunk, a middleware that will allow us to make asynchronous requests in our actions.


Open package.json and add the following libraries to the dependencies section:


    
"dependencies": {
  "react": "16.8.6",
  "react-native": "0.60.5",
  "react-redux": "^6.0.0",
  "redux": "^4.0.1",
  "redux-thunk": "^2.3.0"
},
    
  

Run npm i or yarn to install these new dependencies. Add these libraries to the app by updating App.js as follows:


    
import React from 'react';
import { View } from 'react-native';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import { applyMiddleware, combineReducers, createStore } from 'redux';
import reducers from './app/reducers';


const App = () => {
  return (
     <View />
  );
};

export default App;
    
  

We will use the combineReducers function to combine all app reducers into a single reducer, called rootReducer.


    
const rootReducer = combineReducers({...reducers});
    
  

Next, we will create a store using the createStore function. This function will take the rootReducer and applyMiddleware functions as parameters. To set it up, pass thunk into applyMiddleware.


    
const store = createStore(rootReducer, applyMiddleware(thunk));
    
  

Finally, pass the store object into the <Provider /> component and wrap the <View /> in this component


    
import React from 'react';
import {View} from 'react-native'
import thunk from 'redux-thunk';
import {Provider} from 'react-redux';
import {applyMiddleware, combineReducers, createStore} from 'redux';
import reducers from './app/reducers';

const rootReducer = combineReducers({...reducers});
const store = createStore(rootReducer, applyMiddleware(thunk));

const App = () => {
  return (
    <Provider store={store}>
      <View />
    </Provider>
  );
};

export default App;
    
  

Creating ActionTypes


A typical redux folder structure separates actions, actionTypes, and reducers into separate folders. This project will follow a similar structure.


    
app/
  actions/
      noteActions.js
  actionTypes/
      notes.js
  reducers/
      index.js
      notes.js
    
  

Create the files referenced above with your editor, or use the following command:


    
$ mkdir -p app/actions/ app/actionTypes/ app/reducers/
$ touch app/actions/noteActions.js app/actionTypes/notes.js app/reducers/index.js app/reducers/notes.js
    
  

Let’s take a look at the app/actionTypes/notes.js file to declare our action types. Our app needs to perform two primary actions: fetching and creating notes. Each action requires three different states to represent the status of the request: loading, success, and failure. Therefore, we need to create a total of six actionTypes. In the app/actionTypes/notes.js file, create the corresponding actionTypes.


    
export const FETCH_NOTES = 'FETCH_NOTES';
export const FETCH_NOTES_SUCCESS = 'FETCH_NOTES_SUCCESS';
export const FETCH_NOTES_FAILURE = 'FETCH_NOTES_FAILURE';

export const CREATE_NOTE = 'CREATE_NOTE';
export const CREATE_NOTE_SUCCESS = 'CREATE_NOTE_SUCCESS';
export const CREATE_NOTE_FAILURE = 'CREATE_NOTE_FAILURE';

    
  

Now, we need to create the actions that will fire these actionTypes. Open app/actions/notesActions.js and import the actionTypes we defined above.


    
import * as types from '../actionTypes/notes';
    
  

Creating Actions

To create the action fetchNotes() and dispatch the FETCH_NOTES actionType, add the following to the app/actions/notesActions.js file:


    
export function fetchNotes() {
  return async dispatch => {
    dispatch({type: types.FETCH_NOTES});
  };
}
    
  

Then, use the fetch function to call the notes endpoint on the Rails API. If the response is successful, use the .json() function to convert the content to JSON. If the response is not successful, throw an error.


    
export function fetchNotes() {
  return async dispatch => {
    dispatch({type: types.FETCH_NOTES}); 

     let response = await fetch('http://localhost:5000/notes');
      if (response.status !== 200) {
        throw new Error('FETCH_ERROR');
      }
      response = await response.json();
      dispatch({type: types.FETCH_NOTES_SUCCESS, data: response});
  };
};
    
  

Wrap the request in a try/catch block to capture any failures, and dispatch the FETCH_NOTES_FAILURE actionType when a failure is detected.


    
export function fetchNotes() {
  return async dispatch => {
    dispatch({type: types.FETCH_NOTES});
    try {
      let response = await fetch('http://localhost:5000/notes');
      if (response.status !== 200) {
        throw new Error('FETCH_ERROR');
      }
      response = await response.json();
      dispatch({type: types.FETCH_NOTES_SUCCESS, data: response});
    } catch (error) {
      dispatch({type: types.FETCH_NOTES_FAILURE, error});
    }
  };
}
    
  

With the fetch action complete, we can now implement the createNote action. Add the following to the app/actions/notesActions.js file:


    
export function createNote(note) {
  return async dispatch => {
    dispatch({type: types.CREATE_NOTE});
    try {
      let response = await fetch('http://localhost:5000/notes', {
        method: 'POST',
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({note}),
      });
      if (response.status !== 200) {
        throw new Error('FETCH_ERROR');
      }
      response = await response.json();
      dispatch({type: types.CREATE_NOTE_SUCCESS, data: response});
    } catch (error) {
      dispatch({type: types.CREATE_NOTE_FAILURE, error});
    }
  };
}
    
  

The only difference between the createNote action and the fetchNotes action is the fetch configuration because we need to send the note data that we intend to create. The fetch HTTP method should be changed to POST and the body property should contain the note data, converted to a JSON string.


    
let response = await fetch('http://localhost:5000/notes', {
  method: 'POST',
  headers: {
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({note}),
});
    
  

Setting Up the Reducer

Reducers require an initial state that defines all values that will be used into the store. Open app/reducers/notes.js and create a simple object:


    
const INITIAL_STATE = {
  data: [],
  status: null,
  error: null,
  createStatus: null,
  createError: null,
};
    
  

The data property stores the notes we get from the fetchNotes action. The status and error properties handle the different states of the fetchNotes request. Finally, the createStatus and createError handle the statuses of the createNote request.


Below the INITIAL_STATE object, create a basic reducer function:


    
const INITIAL_STATE = {
  data: [],
  status: null,
  error: null,
  createStatus: null,
  createError: null,
};

export default (state = INITIAL_STATE, action) => {
  switch (action.type) {
    default:
      return state;
  }
};
    
  

The parameter action passes in the action data and type returned from a dispatched action. Let's create the first case for the fetchNotes action:


    
export default (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case 'FETCH_NOTES':
      return {
        ...state,
        status: 'loading',
        error: null,
      };
    default:
      return state;
  }
};
    
  

Each case returns the full app state, which means we need to destructure the current state along with new properties to preserve the full state. Next, create the remaining cases for the fetchNotes action:


    
export default (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case 'FETCH_NOTES':
      return {
        ...state,
        status: 'loading',
        error: null,
      };

    case 'FETCH_NOTES_SUCCESS':
      return {
        ...state,
        status: 'success',
        data: action.data,
        error: null,
      };

    case 'FETCH_NOTES_FAILURE':
      return {
        ...state,
        status: 'failure',
        error: action.error,
      };
    default:
      return state;
  }
};
    
  

FETCH_NOTES is dispatched before our fetch call to the API and ther and sets loading to true so we can display the loading state in the UI.


FETCH_NOTES_SUCCESS is dispatched on success and adds the returned notes to the store.


FETCH_NOTES_FAILURE is dispatched when the action fails and sets the error property to the error returned.


Next, create the createNote cases:


    
switch (action.type) {
  ...   
  case 'CREATE_NOTE':
    return {
      ...state,
      createStatus: 'loading',
      createError: null,
    };

  case 'CREATE_NOTE_SUCCESS':
    return {
      ...state,
      createStatus: 'success',
      data: [...state.data, action.data],
      createError: null,
    };

  case 'CREATE_NOTE_FAILURE':
    return {
      ...state,
      createStatus: 'failure',
      createError: action.error,
    };
  ...
}
    
  

The initial and failure case are similar to the corresponding fetchNotes cases. The CREATE_NOTE_SUCCESS case, however, appends the newly created note to the end of the list of notes. This saves us from making another fetchNotes call to refresh the list of notes with the new note:


    
data: [...state.data, action.data],
    
  

Finally add our reducer to the index file, app/reducers/index.js:


    
import notes from './notes';

export default {
  notes,
};
    
  

We have now created a React Native app, set up Redux, created actions, and created a reducer. In the final article of this series, we build the UI and connect it with Redux.


Check out the Github repo for this tutorial to see the completed app.


---
At FullStack Labs, we pride ourselves on our ability to push the capabilities of cutting-edge frameworks like React. Interested in learning more about speeding up development time on your next project? Contact us.

Let’s Talk!

We’d love to learn more about your project. Contact us below for a free consultation with our CEO.
Projects start at $25,000.

FullStack Labs
This field is required
This field is required
Type of project
Reason for contact:
How did you hear about us? This field is required