Architecture
The Starter Kit architecture is designed to support scalable, modular applications. Built around Redux, it makes it simple to reason about your application's state, and as a result to write maintainable, error-free programs.
The architecture is heavily inspired by Pepperoni App Kit
Redux
The application state and state changes are managed by Redux, a library that implements a pure, side-effect-free variant of the Facebook Flux architecture. Redux and Flux prescribe a unidirectional dataflow through your application. To understand Redux, check out this Cartoon guide by Lin Clark (it's great, not a joke!) and Dan Abramov's Redux course on egghead.io.
Redux helps us with synchronous updating of our state, but it doesn't provide an out-of-the-box solution for handling asynchronous actions. The Redux ecosystem has many possible solutions for this problem. In our application, we use the vanilla redux-thunk middleware for simple asynchronous actions, and redux-loop to handle more complex asynchronicity.
Organizing code
Components
The components
directory should contain React Native JSX components, which take their inputs in as props
. In Flux/Redux parlance the components should be dumb/presentation components, meaning that components should not be connect()
ed to the redux store directly, but instead used by smart/container components.
The components may be stateful if it makes sense, but do consider externalizing state to the Redux store instead. If the state needs to be persisted, shared by other components, or inspected by a developer in order to understand the program state, it should go in the Redux store.
A component may be either written as an ES6 class Foo extends Component
class or as a plain JavaScript function component. Usage of React.createClass
should be avoided, as it will be deprecated in 15.5
If a component implementation differs between iOS and Android versions of the application, create separate .android.js
and .ios.js
files for the component. In minor cases the React.Platform.OS
property can be used to branch between platforms.
Modules
The modules
directory contains most of the interesting bits of the application. As a rule of thumb, this is where all code that modifies that application state or reads it from the store should go.
Each module is its own directory and represents a "discrete domain" within the application. There is no hard and fast rule on how to split your application into modules (in fact, this is one of the most difficult decisions in designing a Redux application), but here are some qualities of a good module:
Represents a screen in the application, or a collection of screens that form a feature.
Represents some technical feature that needs its own state (e.g.
navigator
).Rarely needs to use data from other modules' states.
Doesn't contain data that is often needed by other modules.
Anatomy of a Module
At its simplest, a module contains three logical part: State, View(s) and Container(s). All of these are optional, i.e. a component may or may not a have a View. If a module consists only of a View, though, do consider making it a component instead.
State
The State contains the state of the application, and any actions that can modify that state. State can be data, for example fetched from a server or created by the user in-app, or it may be something transient, such as whether the user is logged into the application, or whether a particular UI element should be displayed or not.
The State part of the module is a Redux Duck - a file that contains a Reducer, Action Creators and the initial state of the application.
Let's take a simple example of an application that displays a number, which the user can increment by pressing a plus button, and decrement using a minus button.
The Redux Ducks pattern aims to keep the code portable, contained and easy to refactor by co-locating the reducer with action creators. For complex modules, the Duck can get quite long and make it difficult to maintain, in which case it should be split into smaller chunks, either by separating the reducer into its own file or by splitting the state into smaller Ducks and combining the reducers using standard Redux split/combine strategies.
View
Typically the View represents the screen in the application. A module may have multiple views, if the part of the application consists of multiple screens, or if the single view is too complex to write in a single file.
Technically speaking the View is identical to a component we define in the components
directory. The difference is the way we use them. Ideally, the View's role is to orchestrate reusable components. The view can be aware of what the application state looks like and which actions update it, whereas a component should not dispatch
things directly, and have their props
API designed around the purpose of the component, not the state of the application.
The View usually has some presentational components and styling, but usually the leaner the view the better. If a view implementation needs to be very different on iOS and Android, separate .android.js
and ios.js
files may be written. However, for maintainability purposes, it is better if the platform-specific implementation can be done on component
level, and the View can remain platform-agnostic.
A View should take all inputs as props
, and should very, very rarely, if ever, be stateful. Instead, the state should be managed in Redux, and injected to the component props by the container.
To continue the Counter example, a view might look something like this:
Container
The Container (or View Container) is responsible for connect()
ing the View component to the Redux store.
Also, the Container is responsible for using the High Order functions with recompose.
Redux connect()
takes in two arguments, first mapStateToProps
which selects relevant parts of the application state to pass to the view, and second mapActionsToProps
, which binds Action Creators to the store's dispatcher so the actions are executed in the right context. These functions are often called selectors.
We think using mapStateToProps
is a good practice, but avoid using mapActionsToProps
in favour of calling dispatch
ourselves in the view. In our experience this leads to simpler, easier to reason about code (and a little less verbose PropTypes on the View).
Every time the app state changes, the Container is automatically called with the latest state. If the props returned by the container differ from the previous props, the connected View is re-rendered. If the props are identical, the view is not re-rendered.
Using the Counter example, the container would be very simple:
Often this file doesn't contain a lot of code, but it's important to define the Container in its own file anyway to be able to support platform-specific view implementations, as well as test the Views and their data bindings separately.
If a View needs data from other modules (i.e. other parts of the application state than the subtree managed by that module), the Container is the correct place to access. In database-speak, this way you can keep your data "normalized" (to a degree), and "join" them when required.
Last updated