Deciding what and how to share

The problem of deciding what and how much to share is based on a few factors, including how different your web and mobile apps are from each other, and whether or not you are starting work on these apps at the same time or if you are extracting shared logic from an existing project.

Our domain — online ordering — is a good use case for shared logic. Our Native and Web apps look fairly different, but in our case the core business logic is the same. The logic of adding something to a cart or checking out with a credit card does not differ because we are on native versus the web. The design of our apps varies greatly but the logic is backed by the same rules and constraints.

We have the advantage of starting work on both applications at the same time, meaning that we can make these kinds of decisions with a blank slate. We took the proactive approach of architecting the apps under the assumption that we share all of our reducers, actions, constants, services, and many utility functions.

But how do we implement shared business logic while also allowing our clients to customize these interactions? When a user taps a product on our app, we might want to redirect them to a different screen in the app, whereas this doesn’t really make sense on the web.

As all of our actions live in the shared package, these actions make liberal use of callbacks that each client can implement as they see fit.

For example, here is an action that is exported by our shared library:

From the React Native side, we can redirect the user to the cart screen only if they were able to successfully add a product.

A side benefit of this approach is that testing the shared library becomes much clearer! We are able to write tests that perform assertions on our shared code without worrying about view-related side effects or anything specific to a particular client. Those clients can implement their own tests for that functionality, if necessary.

It also makes our clients fairly dumb, because they do not need to know anything about how to transform their arguments (in this case, a product) into the representation we need for our API requests. None of our clients need to know about marshalling data around and transforming it for the request, because this lives at a shared level.

Actions vs Action Creators

You see the largest benefit out of shared code and logic if instead of dispatching plain actions you dispatch action creators in the form of functions that return objects or thunks (or promises!). Initially, creating a function for every action seems like a bit of boilerplate. After all, if you can trigger a state change with this:

Why would you want to define a function for every action?

The answer becomes apparent once you begin to use this function from either client. Imagine that in our example from above, our business requirements change and we need to pass in an additional another value. Now imagine that this value lives somewhere else in the global state tree. If we are already calling addProduct as a function, we can easily change its implementation:

As you develop your app with functions, action creators become your API — black boxes that all clients can call without the need to understand their underlying behavior.

A note on Reducers

All of our reducers are exported by our shared package, but we do not export a store. This means that each app creates its own store, implements combineReducers and defines its own middleware stack.

At the outset of the project, we wanted to keep open the option for client apps to add client-specific reducers. We do this very sparingly, but it is a use case we want to keep available in the future. Our current implementation of each app calling combineReducers is very explicit, which works in our favor because each app has a different middleware stack it must define, which is itself an explicit process.

Finally, this means that our shared app does have a store, as we must implement a store if we want to test our actions against a real store and not a mock store. Most of our tests do use a real store, rather than a mock store, so in our shared package we use a custom piece of middleware that logs the actions received by the store.

This lets us make assertions against the types of actions received by our store and the resulting values of our actions without mocking our store. If you’d like to do the same, we’ve open sourced this middleware as as npm package named redux-action-logging.