Inversion of Control with React Native Components

On 26 December 1945, the CFA Franc, a currency still used by many former French colonies in Africa, was created. At the time, after World War 2, the French Franc (the one actually used in France and previously used in the colonies) had been devalued to maintain a fixed exchange rate against the dollar, and France, in its gracious and loving paternity, didn't want the exports from its colonies to receive a lower value, reducing the amount of money available in the colonial economy.

This new currency had a fixed exchange rate to the dollar. To ensure that the exchange rate could remain fixed, the French government had to keep the foreign reserves of these colonies, as a guarantee that foreign currency won't be bought and sold haphazardly by the local government, making it difficult to maintain the fixed exchange rate.

To this day, many of these countries are still required to hold 50% of their reserves in France, to guarantee the stability of the currency. By not having control of these foreign reserves, these "sovereign" states have effectively given away control of their foreign monetary policy to their former colonial masters. They have inverted control of their power.

In software development, Inversion of Control has for some reason become synonymous with Dependency Injection. This is probably because of the wide usage of IoC containers in many popular frameworks, which are used to find and create dependencies to be plugged into modules or classes that need them. But dependency injection is only a type of inversion of control: where control of the creation of dependencies is handed over to something else, instead of classes and modules creating the dependencies.

In React Native, inversion of control can be used to create cleaner, easier to understand components.

Imagine a scenario where Lerato, a software craftswoman, is creating a log book app to record her distance travelled for tax purposes. This app consists of one screen where she enters the date and the kilometres travelled to reach her client:

import React from 'react';  
import { View, TextInput, TouchableOpacity, Text } from 'react-native';  
import LogService from './log-service';

class AddLogEntry extends Component {  
  constructor(props) {
    super(props);
    this.state = {
      distance: 0,
      date: ''
    }
  }

  render() {
    return (
      <View>
        <TextInput 
          onChangeText={date => this.setState({date}) } 
          value={this.state.date} />

        <TextInput 
          onChangeText={distance => this.setState({distance}) } 
          value={this.state.distance} />

        <TouchableOpacity 
          onPress={() => LogService.add(this.state.date, this.state.distance)}>
          <Text>Add</Text>
        </TouchableOpacity>
      </View>
    );
  }
}

She looks at what she has done. And it is good. And it's only the first day.

Later on, a new requirement comes to edit logbook entries. She decides that there are some reusable components that can come out of the first AddLogEntry component, such as an ActionButton button component. So she decides to create this:

function ActionButton(props) {  
  return (
    <TouchableOpacity 
      onPress={() => LogService.add(this.state.date, this.state.distance)}>
      <Text>{props.buttonTitle}</Text>
    </TouchableOpacity>
  );
}

and it is used as follows:

<ActionButton buttonTitle="Add" />  

Done. Button inserted. Reusable component done. Well, kinda.

See, the button is still in control of what happens when pressed. Therefore, when using it in different places, one has to add some-kinda flag to it so the new component can figure out what it can do to handle the context specific logic. Or it can invert control to another component who hold the knowledge of what to do. This can be done by having an event callback method that is passed into the component, such as onPress.

function ActionButton(props) {  
  return (
    <TouchableOpacity 
      onPress={props.onPress}>
      <Text>{props.buttonTitle}</Text>
    </TouchableOpacity>
  );
}

Then the controlling component with the knowledge of what to do will pass a reference of a method to this component:

<ActionButton buttonTitle="Add" onPress={() => doStuff()} />  

This is a trivial example, but illustrates 2 different types of React Native components:

  • Controlling Orchestrating Components: These are components that hold all state of the current scene, as well as handle all logic such as navigating to new scenes, network calls, showing and hiding of other components, etc.
  • User Interaction Components: These components display information to the user, and handle interaction from the user (e.g. text inputs, buttons, date pickers, etc.).

The components which hold all the child components of a particular scene should be the orchestrating components such as the AddLogEntry component above. They should hold all the state of the scene, as well as handle all interactions external to the scene (e.g. with the navigator, network calls, etc.). All child components should then be user interaction components (e.g. the ActionButton component). These should be ignorant components, unaware of the context they are used in. They should receive data through their props, and send user interaction data to the orchestrating components through event callbacks, such as onPress described above, from where the data will be handled according to application logic.

But why are we inverting control from these child user interaction components to the orchestrating components?

  1. Single place for application logic.
    If the application logic for that particular scene is split between different components, e.g. one component does network calls, another component handles navigation logic to load new scenes, and yet another does something else, it would be hard to understand what a particular scene does from just looking at the parent component. One would need to search within each of the child components to get a holistic view.

  2. More reusable user interaction components
    The more ignorant a component is of the different contexts they are used in, the more reusable they are. If they are aware of the contexts they are used in, several conditionals (if-else) need to be placed within these components to handle the context-specific application logic. Ideally, components should be open to extension, but closed to modification of existing logic. Embedding logic requires modification when more contexts are added.

  3. More holistic unit tests
    If all application logic is held in a single component, all this logic can be tested by simulating interactions with the child components and asserting how the orchestrating component handles the data, rather than having the tests for this logic split between various component specific test files (this is assuming shallow tests that do not render all the child components). This way, the tests can be used as a reliable and holistic specification of what that particular scene should do.

In conclusion, giving control of all application logic to the orchestrating parent components results in clearer and cleaner code and tests, making the application's code base more readable and maintainable by other developers over the long run.

Patrick Kayongo

I create and maintain software. Pan-African.

Johannesburg, South Africa