Skip to content

Clientside Architecture

JamieB edited this page Jun 20, 2018 · 2 revisions

Contents

Introduction

Each page of the support site is a self-contained redux/react application. We use redux to handle the internal state of the page and we use react as the presentation layer.

Every redux application has the following components:

  • actions are payloads of information that send data from your application to your store.
  • reducers specify how the application's state changes in response of an action.
  • store holds the application state.

Additionally, since React allows us to describe the UI as a function of the state of the application, we use it as the presentation layer. More information about React/Redux here.

Presentational and Container Components

A common pattern in both the vanilla React and the Redux communities is to divide your code into Presentational and Container components. There are good descriptions of how this works in the Redux docs (the table at the top gives a helpful breakdown), and in this article by Dan Abramov.

An example of this can be seen with the shared ContributionSelection presentational component, which is wrapped at the page level in ContributionSelectionContainer to pass through the Redux state. This way the presentational component can be used anywhere on the site without specific knowledge of a page's state. It can also be used multiple times on the same page when used with scoped actions and reducers, as described below.

In some situations it's necessary to have a container component sit inside a presentational component, as in this case where ContributionSelectionContainer needs to exist inside the purely presentational Contribute component. To achieve this we've used a technique that @RichieAHB outlines here, where you simply pass down the container as a defined prop or as part of the presentational component's children. The React-Redux Provider takes care of getting the store to the nested container component, and the enclosing presentational component can remain oblivious.

There should also be a performance benefit to breaking up the components in this way, as state only gets passed to components that need it. This reduces the need to pass long lists of props down through multiple layers of components, resulting in less needless computation when these props change. It should also mean that each component is better encapsulated, caring only about the data that it needs.

Scoped Actions and Reducers

The main reason for having shared components is to reduce the amount of duplication in the codebase. If we have the same piece of UI across multiple pages, we should be able to reuse the same code. However, if we were only able to share the bare component, and had to rewrite the corresponding reducers and actions for every new instance, we wouldn't really be getting much benefit. We want to share these auxiliary constructs too! However, this results in a potentional problem...

The Problem

For a component to be truly shared, it should be possible to use it multiple times in the same page. Now, it's fairly straightforward to reuse a reducer multiple times in the same store, as you can just drop it into different compartments in the state:

import { combineReducers } from 'redux';

import reusableReducer from './reusableReducer';

const appReducer = combineReducers({
  sectionOne: reusableReducer,
  sectionTwo: reusableReducer,
  sectionThree: reusableReducer,
});

However, this results in a problem. While reducers can be compartmentalised in this way, actions are global in the Redux store. This means that whenever an action that corresponds to reusableReducer is fired, all the places in the state that use that reducer are updated. This is probably not what you want, because, for example, clicking on a reusable checkbox in a given page may end up checking every other checkbox on that page.

The Solution(s)

Fortunately, this topic is covered in the Redux docs, and there are a couple of different solutions offered. We use the second because it allows us to maintain type safety for our actions. In brief, this method involves using action creator and reducer factories, into which you pass the scope (as a string) and get back scoped actions creators and reducers. To see how this works, have a look at contributionSelectionReducer and contributionSelectionActions. There's a great article by AppNexus where they describe setting things up in this way.

Data flow

The data flows in the following way:

  1. The user interact with the UI and he or she generates an action. The action is dispatched to the store via store.dispatch(action).
  2. The store handles the action using the reducer function we defined.
  3. The store saves the new state defined by the reducer in the previous step.
  4. The UI is updated to reflect the last version of the state.

You can find more information about the data flow here.

πŸ™‹β€β™€οΈ General Information

🎨 Client-side 101

βš›οΈ React+Redux

πŸ’° Payment methods

πŸŽ› Deployment & Testing

πŸ“Š AB Testing

🚧 Helper Components

πŸ“š Other Reference

1️⃣ Quickstarts

πŸ›€οΈ Tracking

Clone this wiki locally