React Native Separation of Concerns

Back in 2018, Kimoby decided to invest in a mobile app for its customers. A colleague and I were tasked with building an iOS/Android app on a single code base using React Native. We had no in-house knowledge of this framework, and we learned a lot during the journey to a feature-complete mobile app. In this article, I'll cover one neat little trick we have learned along the way, which is separation of concerns regarding third party libraries. Before I go into detail, a word about our journey into React Native.

We started out trying quite a lot of paradigms and libraries to go with our codebase. There are so many choices in the JavaScript and native worlds that we were initially overwhelmed. Is this the good library for our state management? How will we handle navigation? What’s Expo and why would I need that? The list goes on and on, and new things come out by the day. This is actually one of the main reasons why we came up with our third-party separation idea. If we have to add all these choices on top of the fickle nature of a relatively new ecosystem (the first public React Native release was in June 2015), it becomes clear that we would have a definite gain by separating our third-parties cleanly.

Here’s a short list of some advantages of separating concerns:

  • Easier to rework after breaking changes
  • Very fast third-party library switching and testing
  • Overall drier, cleaner, more homogenous codebase
  • Allows a much cleaner mixing and matching of various third parties of the same type

In our case, we have three concerns that we have completely isolated out of our code. They are:

  • App wide state
  • In-app navigation
  • API calls

Without further ado, let’s dive into our implementations of each of these concerns.

App wide state

An app wide state is a must in any moderately complicated app, and ours is no exception. Initially, we went with MobX as a state container, then switched to use Redux mid-development because it ended up being a better choice.

Quite a hassle, right?

Not so much if we use the following paradigm:

Our react components AND containers have no knowledge of the state. They rely entirely on props, and nowhere is there any mention of Redux/MobX. Where you would usually see a container accessing the state and passing down to the children what they need, we have added an additional layer on top of the container, called the State mapper. It’s basically a very simple file that fetches whatever we need from the store, and passes it to the container as props.

Here’s an example using Redux and Connect:


  import { bindActionCreators } from 'redux'
  import { connect } from 'react-redux'
  import YourContainer from '@containers/your-container'
  import { sendMessage } from '@store/modules/message'
  import { createUser } from '@store/modules/user'
  import { addAlert } from '@store/modules/alerts'

  const mapStateToProps = state => {
    const foo = state.foo
    return {
      ...foo
    }
  }

  const mapDispatchToProps = dispatch => {
    return bindActionCreators({
      sendMessage,
      addAlert,
      createUser
    }, dispatch)
  }

  export default connect(
    mapStateToProps,
    mapDispatchToProps
  )(YourContainer)

With this State mapper, our components and containers are dumber and dryer. Much easier to maintain! If we wanted to try out another state manager, we could simply create another State mapper, using the new library, and pass on what is needed to components without any more hassle.

In-app navigation

We’ve had our fair share of problems using navigation libraries with React Native. Once again, we separated the entirety of the navigation code from the rest of our codebase, in a file pattern we’ve called Screens. Screens are what you pass to your navigation library. They are the only ones to hold any navigation related code, and in turn call on your container/component. We actually prototyped a switch from one library to another extremely fast using this method. Here’s what it would look like:


  import React, { Component } from 'react'
  import { View, BackHandler } from 'react-native'
  import FooContainer from '@containers/foos-container'
  import GenericError from '@generic/error'

  export default class FooScreen extends Component {
  componentDidMount () {
    const fooId = this.props.navigation.state.params.fooId
    this.props.fetchFoo(fooId)
    BackHandler.addEventListener('hardwareBackPress', this.onPressBack)
  }

  componentWillUnmount () {
    BackHandler.removeEventListener('hardwareBackPress', this.onPressBack)
  }

  onPressBack = () => {
    const { someMethod, foo, navigation } = this.props
    someMethod(foo.id)
    navigation.goBack()
    return true
  }

  selectFoo = (foo) => {
    this.props.navigation.navigate({
      key: `foo-${foo.id}`,
      routeName: 'foo',
      params: {
        fooId: foo.id
      }
    })
  }

  render () {
    let {
      error, query, status, navigation
    } = this.props
    return (
      <view>
        <foocontainer status="{status}" query="{query}" navigation="{navigation}" selectfoo="{selectFoo}/">
        {error && <genericerror error="{error}/">}
      </genericerror></foocontainer></view>
      )
    }
  }

As you can see, our Screens are actually React components, but hierarchically, they are parents to containers. This makes navigating a breeze. We also know that screens hold only navigation-centric code, making it easy to find and diagnose.

Bonus interaction with state mappers: Because of the way we’ve separated both, you can point a State mapper to connect with a Screen seamlessly, allowing you to add store information to Screens.

API calls:

This is much more common, but still worthwhile to mention. API calls should be imported as functions from wherever we need them. No knowledge of the underlying network framework should be required by the component/module that needs it. Here is an example of an API file:


  import api from '@api/api'

  export function fetchFoo () {
    return api.get('foo/bar)
  }

And your generic API file could look like this:


  import axios from 'axios'

  const api = axios.create({
    headers: {
      'Accept': 'application/json'
    },
    withCredentials: false,
    baseURL: 'http://www.yourwebsite.com'
  })

  api.interceptors.response.use((response) => {
    return response
  }, (error) => {
      // Handle your errors here
    }
  })
  export default api

Even if you were using React Native’s internal network methods, wrapping them in a custom API file and removing knowledge of the network methods everywhere else is bound to help you diagnose and maintain your code better.

Wrap-up

That’s about it for this little trick! It’s not complicated and saved us a lot of time in the long run. I hope that it can also help you prototype/clean your code faster. React Native is a mighty beast, but we sure had a lot of fun with it at Kimoby.

E-book

Mastering Text Messaging for Business

Discover all you need to know about business texting

Download now