-
Notifications
You must be signed in to change notification settings - Fork 6
UI Data Architecture
The data layer of the UI is built using Redux. We use a few other modules to give more structure and reduce verbosity in the data layer of the app.
This is pretty standard behaviour, but here's where the integration points are:
- The main Redux entrypoint is
src/store/configure.js
. This is where the app storage singleton is configured and middlewares are defined. - On the server,
src/server.js
loads the store configuration and applies it to an empty starting app state. Serverside pre-fetching logic optionally attached to route components (the static methodsGET
,POST
etc) is executed and final app state data is rendered towindow.__INITIAL_STATE__
before sending the page to the client. - On the client,
src/client.js
picks up the starting state data and inflates the store again (usingstore/configure.js
) to continue execution.
Aside from the main store, there are subfolders for the central Redux concepts- actions
, reducers
, selectors
and sagas
. Each contains implementations for features of the app in isolated sections. There is usually a 1:1 correspondence between all folders, but this is not always the case- some reducers may listen to payloads from multiple actions, for example.
We use the following helpers within the storage layer to reduce boilerplate and overall complexity:
redux-action-helper
is a utility library which provides some helper functions useful in declaring redux actions and reducers. The best way to get a feel for what this provides is to take a look at some of the store implementations. In brief, it lets you write your reducers more cleanly as toplevel functions instead of having to include a case
statement for every action, and makes creating simple kinds of action creators easy.
reselect
is a memoizing utility for reading data from Redux state. It will automatically avoid recomputing output if the input arguments haven't changed since last update. So basically it's shouldComponentUpdate()
checks made easy. However, there is a caveat with this: when using selectors between different components the cache is invalidated by each one if they provide different props
to the selector. To workaround this problem you can use re-reselect
to get more control over the caching behaviour.
Here are some guidelines to follow when authoring the data layer:
Constants are the string IDs Redux uses internally to coordinate between actions and reducers. We have 1 big Redux constants file to make things easier to follow. See src/store/constants
.
These should build up a self-documenting reference of the actions that can be performed by the UI (and by services). Each module defines an 'action creator', which is just a function which returns a specially formatted action object. Other than that we define some Flow typings for the input and output parameters so we can validate those in dependant parts of the app. redux-action-helper
has a few action-related helpers we use to reduce verbosity when writing these.
Reducers are the most critical piece of the Redux architecture- they are where you define your storage data structure and update logic.
We define our reducers using redux-action-helper
's createReducer
and createLeaf
helpers, which essentially allow us to define action handling behaviour as individual top-level functions instead of having 1 giant function with a switch
statement in it.
The primary gotcha to be aware of in reducers is don't use mutative operations - such as Array.push
& others. If you alter the previous state in the reducer whilst returning the next updated state, Redux & React & anything else won't be able to tell the state has changed anymore. This can lead to all kinds of weirdness and unexpected bugs.
You will probably notice heavy use of ES6 destructuring in the reducer code, which gives us a quick and reasonably pleasant way of copying objects. In particular, you will always:
- Define your
initialState
and give it a type, but provide{ ...initialState }
when creating your reducer to ensure the values are copied to a new object. - Use
{ ...state, changedProp: newValue }
in your action handlers (seecreateLeaf
) to create a copy of the state object instead of mutating it.
In future we might decide to use Immutable or a similar library to prevent accidental state mutations, but until we have a larger development team and start encountering those kinds of bugs it's probably better to keep our build as lean as possible.
These are pretty simple, they're just functions which accept the (entire) application state and return some (potentially modified) portion of it for access in the UI. In this simplest form, this is all they do.
More complex selectors will benefit greatly from reselect
library as it can be used to memoize (ie. cache) results from previous operations. In essence they are the same though, the data just passes through more transform steps before being returned.
Sagas are where you define the asyncronous actions that occur in your app. Think of them as the "internal actions" which run in response to the "external actions" you've defined in /actions
. They can be composed of any control flow, but usually represent server data fetching actions. For more info, see the redux-saga
middleware notes below.
Note that your reducers will need to be bound to both events from actions
and from sagas
- with an originating action usually setting some variable to indicate that it has begun and _failed
/_success
actions which fire on completion. In most cases you'll want your UI to be aware of all three possible states!
Here are the middlewares currently configured in Redux's store and the purpose of each:
- redux-saga gives us a defined and simple way of managing asynchronous actions.
- redux-thunk is some simple middleware to allow returning functions as actions from the UI. The function will be executed until an action object is eventually dispatched. Note that sagas could be implemented directly on thunks- they are a lower-level concept that allows many things to be more readily integrated with Redux.
- react-router-redux isn't something you should have to interact with, it just tracks the URL in Redux so that the Redux state & time-travel debugging also syncs with the location.
- remote-redux-devtools is a development plugin which handles connecting your app to RemoteDev. See the app README for more details on debugging.
About this codebase
For new contributors