Architecture V1
Table of Contents
Diagram
This is how the architecture started out and is still used in various places.
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