All Blog Posts

React-Native: Taking Control With Redux

'Nightmarish heaps of spaghetti code'. That's how our developers describe their earliest React-Native prototypes. Step in Redux, and we find ourselves developing React-Native apps with ease. Here's how you too can take back control with Redux.

12 min read

Development

React Native

3 Sided Cube is still learning the ropes of app development with ReactNative. All our ReactNative developers were until a few months ago solely focused on native app development, either using Java for Android, or Swift/Objective C for iOS.

Moving over to Javascript has been challenging: development paradigms that were effective natively don’t necessarily translate across well into the vastly different world of Javascript.

Our early ReactNative prototypes were nightmarish heaps of spaghetti code, order barely discernible through the chaos. But over time we have learned to embrace our new language, to hone new ways of working, regaining order from chaos and once again taking control of our development destinies!

Taking control with Redux.

In its own words, Redux is a “predictable state container for Javascript apps”. To think of it in this way is a bit abstract, even with the assistance of Redux’s really quite excellent documentation. From our experience, throwing devs new to the project in at the Redux deep end hasn’t been very productive: there is too much new stuff to take in to truly appreciate the mechanics of what is going on under the hood. Instead to attain true enlightenment one must personally undertake the journey from chaos to order…

In the beginning our code was, to say the least, messy. We were each suffering from the inertia of coming into Javascript from different platforms with development practices which we were reluctant to give up. Although we liked the idea of React and how it worked, it wasn’t obvious to any of us the best way of communicating between components, or of updating components in response to changes in state. Even with a fairly simple app it was difficult to reason about the flow of data or the state of a given component at any point in time.

Hence, after some Google research, we settled on Flux. We liked Flux. It was encouraging that it was specifically addressing the confusion we had been facing and it introduced us to the concept of actions and stores. Actions describe simple mutations to state, and stores encapsulate that state. For instance, an “Add 10% interest” action might update the amount of money held by a store representing a bank account.

Most importantly, Flux was strict about how these actions and stores should interact, using a so-called unidirectional data flow, where React components dispatch actions which update stores which in turn inform the components of any changes to state. When you’re new to a language, strictness is good, as it drastically reduces the number of ways you can shoot yourself in the foot.

With Flux, we were getting pretty good at staying on top of the now-decreased foot-shooting opportunities. But we found we were writing a lot of boilerplate code. Stores had lots of code in common, especially those managing collections of model objects. Furthermore, every asynchronous task had corresponding actions for when that task is starting, when it has succeeded, and when it has failed (and possibly whenever there’s a progress update), all of which were created and handled largely identically.

To stay on top of this, we gradually began refactoring our code, identifying commonalities which could be used by all our stores, and creating increasingly generic abstractions around how we were creating and dispatching our actions, especially our asynchronous ones. It was around this point we coincidentally (and mercifully) discovered Redux, and found that it essentially represented the result of taking our refactoring to its logical extreme.

Like Flux, Redux operates using the concept of actions and stores, but differs in a few crucial ways. Most importantly, in Redux there is only one store, a global store, acting as a single point of truth as to the state of an app. Again, actions represent the mutation of state, but in Redux just a single function — a reducer — is responsible for updating the global state object in response to incoming actions, with each action handled sequentially, and each acting only upon the output state of its predecessor.

On the face of it this sounds unmanageable. Just a single function responsible for handling potentially thousands of actions? In practice most parts of the global state are somewhat independent from all other parts, and the reducer function can be composed from multiple sub-reducers in a process termed reducer composition. Furthermore, in writing a reducer you are quite literally defining the shape of your state object, and it becomes glaringly obvious when two separate parts of state are in fact near-identical and hence can share a reducer, as is the case, say, for asynchronous data, or for lists of data.

Crucially, because all actions pass through a single pipeline and state is held in a single object it is amazingly easy to reason about the effect an action will have. It also makes it trivial to plug-in new functionality and intercept and react to dispatched actions in custom ways. Functionality of this kind is referred to as middleware, and can be quite breathtakingly powerful. In fact, it was middleware which really hit home the true utility of Redux to our team. Because just a vanilla implementation of Redux already aids immensely in code clarity and structure, it felt almost as if we were getting all the auxiliary benefits — benefits such as hassle-free testability, persistence, and crash reporting — for “free”, with very little development effort on our part.

Redux middleware

Some of the middleware we’ve made most use of are as follows:

  • redux-thunk – A thunk is nothing more than a function accepting the Redux dispatcher and state as parameters. As such, it allows for conditionally dispatching actions based on current state. For instance, we may choose to not make a new API request if we can tell from our state that the data we already have is reasonably up-to-date.
  • redux-promise-middleware – One of a number of middleware implementations designed to deal with Javascript promises – our preferred way of dealing with asynchronous tasks. We will discuss how we deal with tasks of this kind in more detail in the next section.
  • redux-logger – Allows for logging of each dispatched action and the state before and after it was handled by reducers. Slow — you will want to limit its usage in a release app — but it is invaluable for understanding data flow during development.
  • redux-persist – Supports selectively persisting chunks of state so that they are automatically reloaded next time the app is launched.
  • redux-form – Not a piece of middleware, but makes validating forms and correctly updating related UI very easy.

For middleware to work effectively, it’s import to construct our actions so that they conform to a standard structure. We found Flux Standard Actions (FSA) were fine for our purposes, with each action consisting of (i) a type identifier, so that reducers can recognise it, (ii) a data payload, (iii) an error flag, and (iv) possibly some extra metadata. Adopting FSA allows various kinds of middleware to work out the box without extra configuration, but bringing in an enforceable standard also brought extra clarity to the codebase (and one less way to harm our feet).

Asynchronous Actions

The vast majority of the actions currently implemented in our apps are asynchronous in nature. They include API calls, invocations of native code, user input processing, and most other non-trivial functionality. Actions of this type necessitate the use of some of the middleware we mentioned above. Namely, redux-thunk is required so we can check whether we even need to perform the asynchronous task in the first place, and redux-promise-middleware so that we can easily handle asynchronous tasks taking the form of promises. redux-promise-middleware automatically detects FSAs with Promise payloads, and dispatches PENDING (immediately), SUCCESS (if the promise is resolved), and FAILURE (if the promise is rejected) actions automatically at the appropriate times.

However, as soon as asynchronicity entered the mix, we found it difficult to understand what exactly was happening to our actions once they’d been passed into the magical Redux dispatcher. Whereas a traditional synchronous action is simply passed straight through to the reducer, the asynchronous flow looks more like the diagram below.

Asynchronous Actions

It takes some careful thought and a few attempts at diagrams like this to realise the redux dispatcher is not so magical after all, but there is a delay before that eureka moment hits.

We also found 90% of our asynchronous reducers were exactly the same. They all managed areas of state comprising the following:

  • The data returned by the promise.
  • Any errors encountered by the promise.
  • A flag indicating whether the promise was still in progress, to avoid executing the same task twice concurrently.
  • A flag indicating whether or not the data has been invalidated, meaning it is stale and we should fetch more data at the next opportunity.
  • A timestamp of when the promise was last executed.
  • A count of how many times the promise has failed consecutively, so we can identify problematic actions and e.g. throttle or suppress future invocations.

In terms of state shape, this looks somewhat like the following:

/**
 * A standard state structure for asynchronously retrieved data.
 */
export const initialState = {
  isFetching: false,
  lastFetchTime: 0,
  data: null,
  error: null,
  isInvalidated: false,
  consecutiveFailureCount: 0
};

 

Because these parts of state are so similar, we were able to write a shared asynchronous action reducer to handle them, like follows:

export function asyncActionReducer(handledAction, state = initialState, action) {
  switch (action.type) {
    case handledAction + "_" + reduxConstants.PENDING_SUFFIX:
      return {
        ...state,
        isFetching: true,
        lastFetchTime: action.meta.timestamp
      };
    case handledAction + "_" + reduxConstants.SUCCESS_SUFFIX:
      return {
        ...state,
        isFetching: false,
        data: action.payload,
        error: null,
        isInvalidated: false,
        consecutiveFailureCount: 0
      };
    case handledAction + "_" + reduxConstants.FAILURE_SUFFIX:
      return {
        ...state,
        isFetching: false,
        error: action.payload,
        consecutiveFailureCount: state.consecutiveFailureCount + 1
      };
    case handledAction + "_" + reduxConstants.INVALIDATE_SUFFIX:
      return {
        ...state,
        isInvalidated: true
      };
  }
  return state;
}

 

This asynchronous reducer could then be composed into larger reducers by simply binding it to whatever action it handles.

const dataReducer = combineReducers({
  identity: asyncActionReducer.bind(this, actions.FETCH_IDENTITY),
  account: asyncActionReducer.bind(this, actions.FETCH_ACCOUNT),
  purchases: asyncActionReducer.bind(this, actions.FETCH_USER_PURCHASES)
});

 

Now, we are updating our asynchronous areas of state in a standard, consistent way, with a shared reducer. This not only makes it simpler to build responsive UI components that correctly show error / in progress / stale states, but also, because all our asynchronous state conforms to the same shape, we can use the exact same function to decide whether we should execute a given asynchronous task:

export function shouldPerformAsyncAction(state) {
  if (!state) {
    // State should always be defined already, but if not then do a fetch to initialise it
    return true;
  }

  // Determine whether the last fetch request occurred in the last 30 seconds.
  let didFetchRecently = Date.now() - state.lastFetchTime < 30000;
  if (state.isFetching) { 
    // If we are fetching, then don't fetch again (wait for the old fetch to finish) 
    return false; 
  } else if (state.consecutiveFailureCount >= 3 && didFetchRecently) {
    // If we have recently failed 3 times or more consecutively, then suppress fetching (to avoid infinite spinning when dealing with broken API endpoints).
    return false;
  } else if (!state.data) {
    // If we don't have data then always fetch
    return true;
  }

  // Otherwise, only fetch if the data we have has been invalidated
  return state.isInvalidated;
}

 

Typically, we call this in our “thunks” to decide whether our asynchronous task even needs to be invoked. Of course, a lot of the above state shape is specific to the needs of our particular tasks, but really demonstrates how the complexity of asynchronous behaviour can be reduced. Furthermore, there is nothing preventing multiple asynchronous state shapes being used in the same app: we do this to handle the data we retrieve from paginated API endpoints in a consistent way. Now when we build a new UI component, possibly reliant on a new API call, we have the confidence that it is being built using just a small amount of infrastructure code already used throughout our app and that we can easily inspect and reason about it due to the nature of Redux.

Since adopting Redux we have felt tremendously productive for the first time since moving from native. It feels truly as if we are working with the Javascript language, using a paradigm that seems like it wouldn’t be as natural in Java / Objective C. It remains to be seen whether we discover some further level of productivity beyond Redux, but for now at least we are happy to have retaken control of our code and overcome our initial struggles with acclimatising to this new way of developing apps.

Published on April 27, 2016, last updated on November 25, 2020

Give us the seal of approval!

Like what you read? Let us know.

0

Tags

Join us for a Mobile Disco

Bring your ideas to life...

Find Out More