DEV Community

Andrew Stuntz
Andrew Stuntz

Posted on

Keeping API interactions sane while co-developing a React and React Native Application

React and React-Native oh my!

A tale of two applications

I recently worked on an application that had two separate public facing applications. One part was a web application, the other a phone application. Written in React and React-Native respectively. This use case is not that uncommon. It was fairly apparent from the get go that eventually we would need most features to be present in both the web application and the phone application. I figured we'd use Redux for state management but was pretty lost when it came to "interactions" with our back end from React and React-Native.

The nice part is that its just Javascript so there is nothing fancy going on here.

React-Native lifecycle hooks are the same and overall we wanted to make our mobile app experience much like our web app experience.

After some research we decided to use Redux-Saga to make our lives easier and we were able to copy most (if not all) of the api interaction between the two apps. There is even a thought that we may be able to get away with completely separating the API interaction from our views. Though, that may be a little more difficult than it sounds. Either way to the code examples!

But only a single backend

Currently the back end is written in Rails and was built as an API only application. This lets us completely separate the back end and front ends. We were able to statically host all of our JS and CSS for the web application. We serve zero html from our web server. How cool is that!? (I guess it's not that cool it is an SPA after all...) While the entire API does not follow the JSON API spec it does serve JSON API compliant JSON. Which I think adds a sense of consistency to our API. I'd like to continue to build out the JSON API compliance of the API but that is neither here nor there.

Here is an example of a Saga that we used to interact with the back end.

src/sagas/inventory.js

import { call, put, takeLatest } from 'redux-saga/effects'
import { defineTypes } from '../helpers'
import ProductsEndpoint from '../persistance/ProductsEndpoint'

// I use a custom Fetch implementation that wraps my requests to force
// authorization when required, it also forces JSON responses to be camelCased and // returns a consistent response from each request that we make.
import { fetch } from '../http/reduxFetch'

// This helper lets us have consistent reducer names, though it ended up not being perfect. 
const inventoryTypes = { ...defineTypes('INVENTORY')  }

export const initialState = {
  loading: false,
  loaded: false,
  stocked: false,
  error: {},
  products: [],
  inventories: []
}

// Each sagas exports actions and a reducer, which lets us combine them together // or use them across the app. Since they are all called 'actions' and 'reducer' // consistently it's super easy to import them at any point or from any component.

export const actions = {
  getInventory () {
    return { type: inventoryTypes.REQUEST }
  },
  toggleStock () {
    return { type: 'TOGGLE_STOCK' }
  }
}

export const reducer = (state = initialState, action) => {
  switch (action.type) {
    case inventoryTypes.REQUEST:
      return { ...state, started: true, loading: true }
    case inventoryTypes.DESTROY:
      return { ...state, loading: false }
    case 'TOGGLE_STOCK':
      return { ...state, stocked: !state.stocked }
    case inventoryTypes.FAILURE:
      return { ...state, loading: false, error: action.error }
    case inventoryTypes.SUCCESS:
      return { ...state, loading: false, loaded: true, inventories: action.inventories, products: action.products }
    default:
      return state
  }
}

// JSON API led to some heavy selectors. It would be prudent to add an inventory // selector or an attribute selector, but add it to the list!

export const selectors = {
  inventories: (store) => {
    return store['inventory'] && store['inventory']['inventories']
  },
  products: (store) => {
    return store['inventory'] && store['inventory']['products']
  },
  stocked: (store) => {
    return store['inventory'] && store['inventory']['stocked']
  },
  loaded: ({ inventory }) => {
    return inventory && inventory['loaded']
  },
}

export function * inventorySaga () {
  try {
    const products_response = yield call(fetch, ProductsEndpoint.index())

    if (products_response['data'] ) {
      yield put({ 
        type: inventoryTypes.SUCCESS,
        products: products_response.data 
      })
    } else {
      yield put({
        type: inventoryTypes.FAILURE,
        error: products_response['error'] 
      })
   }
  } catch (error) {
    yield put({ 
        type: inventoryTypes.FAILURE, 
        error 
    })
  }
}

// All our top level Saga's are exported as a rootSaga. This lets us import and 
// combine all of our sagas in a rootSaga file so we can keep everything 
// organized and check name spacing.

export function * rootSaga () {
  yield takeLatest(inventoryTypes.REQUEST, inventorySaga)
}

Overall this is pretty simple it looks like a Redux with some fancy yielding and generator functions. The most complicated thing going on above is the Saga itself. If you don't know too much about generator functions, you should check this guy out, it explains things nicely.

https://davidwalsh.name/es6-generators

In simple terms though, they basically step through functions and allow you to call .next() on them to run the function until it hits the next yield keyword.

Redux Saga is a side effect library for Redux that runs generator functions based on action calls. So you don't have to have your business logic in your reducers. It lets you run any Saga on any call to any action, you can also use many types of effects that let you take just one Action (so you don't repeat the API call), or just the last action call (so you can call multiple times and it just takes the latest), there are many options but these are the basic ones.

Here is some Redux-Saga documentation: https://github.com/redux-saga/redux-saga

Another cool thing about Redux Saga is that your Component has to know nothing about it. Since your expose your selector to the Component via Redux. Convenient eh!? They're API calls that are not dependent on Component Lifecycle hooks* or call backs. In fact we could hydrate the entire store and then render views one at a time, and the components would be none the wiser. We can also call the API endpoint once and then just select out what we need from the response as we go instead of recalling the API multiple times.

*Note at some point we do have to make API calls that have dependency's on components being rendered, but this vastly reduces the amount of lifecycle hooking we are doing. I also tend to have parent components that I hook into and the children components then select out of the store that is filled based on the API call from the one lifecycle hook in the parent component.

Now that we have the data how can we use this? Well, just need a connected component and all of a sudden we can pull data out of our selectors.

In a strictly React application:

src/components/Inventory.jsx and src/components/Inventories.jsx

import React, { Component } from 'react';

class Inventory extends Component {

  render() {
    return (
      <div>
        <div>
          { this.props.inventory && this.props.inventory['name']
        </div>
        <div>
          { this.props.inventory && this.props.inventory['attribute']
        </div>
      </div>
    )
  }
}

import React, { Component } from 'react';
import { selectors, actions } from '../sagas/inventory';

class Inventories extends Component {
  componentDidMount() {
    this.props.getInventory()
  }

  render() {
    return (
      <div>
        { this.props.inventories && this.props.inventories.map(inventory => {
          <Inventory inventory={inventory} />
        })
      </div>
    )
  }
}

// Since 'Inventories' is connected, any time we change inventories in our store,
//they're gonna get updated through the component. 

const mapStateToProps = (store) => ({
   inventories: selectors.inventories(store)
});

const mapDispatchToProps = (dispatch) => bindActionCreators(actions, dispatch);

export default connect(mapStateToProps, mapDispatchToProps)(Inventories);

Or in a React-Native Application

src/components/Inventory.jsx and src/components/Inventories.jsx

import React, { Component } from 'react';
import {
  View,
  Image,
  Text,
  StyleSheet,
  ScrollView,
} from 'react-native';

export default class Inventory extends Component {

  render() {
    return (
      <View>
        <Text>
          { this.props.inventory && this.props.inventory['name']
        </Text>
        <Text>
          { this.props.inventory && this.props.inventory['attribute']
        </Text>
      </View>
    )
  }

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import {
  View,
  Image,
  Text,
  StyleSheet,
  ScrollView,
} from 'react-native';

import { selectors, actions } from '../sagas/inventory';
import Inventory from './Inventory'

class Inventories extends Component {
  componentDidMount() {
    this.props.getInventory()
  }

  render() {
    return (
      <View>
        { this.props.inventories && this.props.inventories.map(inventory => {
          <Inventory inventory={inventory} />
        })
      </View>
    )
  }
}

// Since 'inventories' are connected, any time we change inventories in our store, they're gonna get updated through the component. 

const mapStateToProps = (store) => ({
   inventories: selectors.inventories(store)
});

const mapDispatchToProps = (dispatch) => bindActionCreators(actions, dispatch);

export default connect(mapStateToProps, mapDispatchToProps)(Inventories);

Almost the same exact components between React-Native and React. But all the business logic is thrown into the Saga so you don't have to worry too much about what is going on outside of the Component. Pretty easy to share the logic of the Saga and display what we want.

Overall we run through a quick example of sharing API interactions between a React-Native and React application using Redux-Saga's. It allows you to build out React and React-Native apps quickly against a single API but encapsulate and share business logic between the two applications even though the Components are completely separate.

*I'm by no means a JS/React or React Native expert. Let me know if you see anything funky or have any tips to help me elevate my JS or React game.

Top comments (0)