Skip to main content

Architecture V1

Table of Contents

Diagram

This is how the architecture started out and is still used in various places.

Architecture Diagram V1

Containers

A container provides its connected component with the pieces of the data it needs from the store, and the functions it can use to dispatch actions to the store. It uses the connect function from react-redux which connects a React component to the Redux store and returns a wrapped component.

Container Example

// LoginContainer.ts
// imports omitted

const mapStateToProps = (state: RootState) => {
const isAuthenticated = userApiSelectors.isAuthenticated(state)
return { isAuthenticated }
}

const mapDispatchToProps = (dispatch: Dispatch) => ({
redirect: (path: string) => dispatch(uiEvents.redirect(path)),
})

export default connect(mapStateToProps, mapDispatchToProps)(LoginPage)
// LoginPage.tsx

type Props = {
isAuthenticated: boolean
}

const LoginPage: React.FC<Props> = ({ isAuthenticated }) => {
return isAuthenticated ? <p>You are authenticated!</p> : <p>Please login below</p>
}

LoginContainer can now be used as a React component.

// SomeOtherComponent.tsx

<LoginContainer />

Container Problems and Pain Points

While containers can be used more responsibly, they haven't in the past which lead to the following problems.

Prop Drilling

Reasons to avoid prop drilling:

  • Tedious and the prop types/interfaces need to be updated for every component that gets drilled.
  • Causes unecessary re-renders in components that got only received a prop to pass it down (see below).
  • Clutters git diffs with work not entirely relevant to a feature or fix.

Unecessary component tree re-renders

Dropping all state and dispatch functions in the container and prop drilling to feed children components leads to unecessary re-renders. Let's use the following example taken from the code base, but simplified for demonstration purposes.

├── InvoicesPageContainer
│ ├── InvoicesPage
│ │ ├── InvoicesPage
│ │ │ ├── Layout
│ │ │ │ ├── LayoutContent
│ │ │ │ │ ├── Content
│ │ │ │ │ │ ├── ViewOpenInvoice
│ │ │ │ │ │ │ ├── CurrencyField
│ │ │ │ │ │ │ ├── PaymentAllocation
│ │ │ │ │ │ ├── ListingTemplate
│ │ │ │ │ │ │ ├── SearchFilter
│ │ │ │ │ │ │ ├── SearchFilter
│ │ │ │ │ │ │ ├── OpenInvoiceTable
// InvoicePagesContainer.ts

const mapStateToProps = (state: RootState) => {
const id = uiInvoiceSelectors.getCurrentInvoice(state)

return {
currentInvoice: reconApiSelectors.getInvoiceById(state)(id),
openInvoiceFilters: uiInvoiceSelectors.openInvoiceFilters(state),
}
}

const mapDispatchToProps = (dispatch: Dispatch) => ({
openInvoice: (id: string) => {
dispatch(uiInvoiceEvents.invoiceOpened({ type, invoiceId }))
},
})

export default connect(mapStateToProps, mapDispatchToProps)(InvoicesPage)

Let's assume that the currentInvoice is drilled as a prop from InvoicesPageContainer to ViewOpenInvoice and OpenInvoiceTable components (6 and 7 levels deep respectively) and those are the only components that require access to the currentInvoice. When the value of currentInvoices changes the entire tree is re-rendered when we only needed a re-render of ViewOpenInvoice and OpenInvoiceTable.

This could be solved by creating specific containers for both ViewOpenInvoice and OpenInvoiceTable but that is a lot of work. The provider pattern elimates the need to prop drill but also has the same problem of unecessary re-renders.

Solution

Refer to Future Direction for more information how we can solve both of these problems.

Original Project Structure

src
├── constants
│   ├── general.js
│   └── routes.js
├── index.js
├── serviceWorker.js
├── state
│ ├── modules
│ │   ├── api/*
│ │   └── ui/*
│   ├── history.js
│   ├── middleware
│   ├── root.js
│   └── store.js
├── theme
│   ├── icons
│   ├── images
│   └── styles
├── utils
└── views
├── components
│   ├── App
│   ├── atoms
│   ├── molecules
│   └── organisms
│   └── pages
│   └── templates
├── containers
├── routes.jsx
└── utils