Front-end Architecture

While the Rails way of UJS (Unobtrusive JavaScript) is sufficient for smaller projects when it comes to interaction-heavy applications, the former solution, along with jQuery and other popular methods is not very elegant, not to mention hardly maintainable. Facebook Flux architecture accompanied with React introduces an unidirectional flow which adds some restrictions which result in a more clear and consistent code. Our preferred Flux implementation is Redux. It is the most popular implementation and has a large community.

In some projects we use Relay - a GraphQL client with React integration. It completely replaces Redux in most of the application. Redux is only used for components which have a large internal state, such as forms.

Redux

Application state is kept inside an instance of a store instead of multiple stores. The store state can be imagined as a tree containing nested states of other stores.

{
  exercises: [{ id: 1, name: 'Pull-ups' }],
  page: 1
}

The given example above would contain two store states that correspond to exercises and page. These states are returned by reducers.

createStore(reducer, initialState, enhance) function is provided to create the one and only store for the application. The returned store is passed to a Provider

Provider is a react-redux React component that wraps the whole given Redux application. It takes the created store instance and provides it to it’s children via getChildContext().

connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) is provided as an ES7 Decorator for updating components with the newly reduced state of the store. It acts as a Flux Container, returning a renderable React Component, which passes different props to an underlying child container corresponding to the output of the given callbacks.

combineReducers(*reducers) provides a shorthand for nesting multiple reducer functions. A corresponding example is given below.

function mainReducer(state = {}, action) {
  return {
    exercises: loadExercises(state.exercises, action),
    page: setPage(state.page, action)
  }
}

reducer(state = {}, action) is a plain function that returns the previous or the new state after handling the action payload.

The flow consists of using the store’s dispatch method that calls the top reducer passing the given action payload, which subsequently calls the underlying reducers. The connected components handle the store’s update event and recalculate the properties causing a re-render.

alt text

  • Upsides:
    • Easily extendable with middleware.
    • Extensive guides introducing to the Flux-way of writing reusable code.
    • LARGE amount of community contributed libraries.
    • Promotes reducer reuse.
  • Downsides:
    • Steeper learning curve (async action flow, redux-thunk, redux-actions).

Tips

  • Differentiate Components from Containers.
  • Consider adding more Containers to avoid passing unused props through multiple layers.
  • Prefer Container composition over smart components. See containers/FilterLink.js.
  • Create eventHandlers in Containers passing them down to components.
  • Extract the Web Request logic into a separate class to maximize re-usability when end-points differ.

Relay

We use Facebook GraphQL client - Relay. It provides helpers for wrapping React components with GraphQL functionality. The main benefit is that each component defines what data it needs and Relay combines everything into a single GraphQL query, which is then sent to GraphQL server. It also ensures data consistency when using GraphQL mutations.

Styling React applications

Use styled-components to style React components. This has several advantages over using regular SCSS files:

  • Style and component are in the same place.
  • Clear where style is used.
  • Easier to find and remove unused styles.
  • Much smaller chance to break other parts of the application when changing a style.
  • Easier to use component because it comes already styled.

General tips

  • Prebind your instance dependent methods instead of binding them directly inside the render method. This prevents creating new functions every time your component renders. We use ES2015+ class properties to achieve this.
  • Prefer function expressions over function declarations:
// Bad
function someFunction() {
  // ...
}

// Good
const someFunction = () => {
  // ...
}

Application structure

Our React/Redux/GraphQL projects have the following structure:

  • Features are grouped into domains.
  • Every domain has its own directory.
  • Each domain has a dedicated components/ directory which contains domain’s components.
  • Each domain has a dedicated mutations/ directory which contains domain’s mutations.
  • Code generated by Relay (domain/components/__generated__, domain/mutations/__generated__) should not be added to source control.
  • Common components are placed in a global common/components/ directory.
  • Application’s store is placed in a store.js file in a global directory.
  • A container is usually placed in the same file as the component that is being contained. Such component is then called a “connected component”.
  • A domain usually has separate files for actions, action types, selectors, reducer, etc.
  • Each domain has a dedicated spec/ directory which contains domain’s tests.
  • Each domain has an index.js file which acts as an aggregate root of the domain which exposes its functionality. If a domain A requires some functionality from a domain B the functionality should be accessed via the aggregate root of a domain B and not directly.

Example directory structure:

app
├── common
│   ├── components
│   │   ├── CommonComponent.jsx
│   │   └── index.js
│   ├── spec
│   │   └── CommonComponent.spec.jsx
│   └── index.js
├── domainA
│   ├── components
│   │   ├── ComponentA.jsx
│   │   └── index.js
│   ├── spec
│   │   ├── actions.spec.js
│   │   ├── ComponentA.spec.jsx
│   │   ├── reducer.spec.js
│   │   └── selectors.spec.js
│   ├── actions.js
│   ├── actionTypes.js
│   ├── index.js
│   ├── reducer.js
│   └── selectors.js
├── domainB
│   ├── components
│   │   ├── __generated__
│   │   ├── ComponentB.jsx
│   │   └── index.js
│   ├── mutations
│   │   ├── __generated__
│   │   ├── MutationB.js
│   │   └── index.js
│   ├── spec
│   │   ├── ComponentB.spec.jsx
│   │   └── MutationB.spec.js
│   └──index.js
├── index.js
└── store.js

Domain structure

Our way of organizing the application is grouping components and their code into features, also known as domains. This way we increase cohesion, loosen the coupling and promote encapsulation.

Considerations to help structuring your domains:

  • Prefer exposing components directly. Exposing many components through the components module may make it harder to identify the domain scope’s boundaries.
  • Prefer exposing actions that comply with your domain’s interface directly, e.g. exporting open and close actions in a modal domain gives you a more concise interface. Large, core domains might expose their actions through a nested module.
  • Prefer exposing a single reducer that may nest multiple reducers of your domain. This prevents global store pollution and promotes reusable reducer extraction.
// DomainA index.js
export { ComponentA, ComponentB } from './components'
export { actionA, actionB } from './actions'
export { default as reducer } from './reducer'

...

// some other file that uses DomainA
import { ComponentA, actionA } from '~/domainA'
// or if above does not fit:
import * as domainA from '~/domainA'