Skip to main content

State Management

We use Redux for global state management with the official React binding.

We use Redux Toolkit for reducing boilerplate and to make Redux fun again.

The examples below are based on the src/modules/api/portfolio module to stay consistent with the examples in Testing with Jest

There is a video on Google Drive that walks through creating a new state module.

Constants

Example

const { REACT_APP_API_URL = '' } = process.env

export const NAMESPACE = 'portfolio'

const BASE = `${REACT_APP_API_URL}/portfolios`
const ENTITY = `${BASE}/:id`

export const ENDPOINTS = {
BASE,
SUMMARIES: `${BASE}/summaries`,
}

Events (AKA Redux Actions)

Events are simply redux actions, named "events" for parity with the CQRS pattern in our backend services. Also "event" describes the concept better than "action" as they only describe what has happened. Events are payloads of information that send data from the app to the store. They are the only source of information for the store. You send them to the store using store.dispatch().

Example

The reducer object keys are the events. The function value is the reducer.

const portfolioApiSlice = createSlice({
name: `${NAMESPACE}Api`,
initialState,
reducers: {
portfolios_request: (state, { payload }) => {},
portfolios_success: (state, { payload }) => {
// ...reducer
},

Epics

An Epic is the core primitive of redux-observable. It is a function which takes a stream of events and returns a stream of events. Events in, events out. Compose and cancel async events to create side effects and more.

Example

export const apiFetchEntity = (action$: $TSFixMe, state$: $TSFixMe, { get, catchRestError }: $TSFixMe) =>
action$.pipe(
ofType(events.portfolio_request),
mergeMap((action: AnyAction) => {
return get(ENDPOINTS.FETCH_PORTFOLIO, state$, { id: action.payload }).pipe(
pluck('response'),
map(res => {
return events.portfolio_success(transformers.transformGetPortfolioResponse(res))
}),
catchRestError(action),
)
}),
tag('portfolio/epics/apiFetchEntity'),
)

Reducers

Reducers specify how the application's state changes in response to events sent to the store. Remember that events only describe what happened, but don't describe how the application's state changes.

Example

const initialState = {
allIds: [], // Used for ordering
byId: {},
}

const portfolioApiSlice = createSlice({
name: `${NAMESPACE}Api`,
initialState,
reducers: {
portfolios_request: (state, { payload }: $TSFixMe) => {},
portfolios_success: (state, { payload }) => {
/** @todo refactor when API is updated with flatten portfolios */
const portfolios: $TSFixMe[] = pipe(map(prop('portfolios')), flatten)(payload)
state.allIds = map(prop('id'), portfolios)
forEach((p: any) => {
state.byId[p.id] = p
}, portfolios)
},
},
})

Selectors

A selector is a function that derives data from the Redux store. Reselect is a simple library for creating memoized, composable selectors.

Example

export const getSlice = (key: string) => createSelector([`api.${NAMESPACE}.${key}`], identity)

/**
* Get a list of portfolio IDs
*
* @returns {Array} List of portfolio Ids
*/
export const getPortfolioIds = getSlice('allIds')

/**
* Get a list of portfolio objects
* Correctly orders portfolios based on API response using `allIds` index
*
* @returns {Array} List of portfolio objects
*/
export const getPortfolios = createSelector([identity], state =>
map((id: any) => path([id], getSlice('byId')(state)), getPortfolioIds(state)),
)

Transformers

A transformer is a function that transforms and sanitizes data between API requests and responses. An example of one type of transformation is currency. The backend works with currency in decimals, while the client works with currency in cents. Transformers are used to convert decimals to cents on responses and vice-versa for requests.

Transformsers are created using the Morphism Library

Example

The below example is collabsed with ... to keep things shorter.

const getPortfolioResponseSchema = {
id: 'id',
createdByUserId: 'createdByUserId',
...
/** @todo handle unmanaged */
contractContainer: ({ contractContainer }: any) => {
const type = get(contractContainer, 'type', '')
const terms = get(contractContainer, 'value.terms', {})
return (
contractContainer && {
type,
value: {
terms: {
...
damageDepositAmount: toCents(get(terms, 'damageDepositAmount', 0)).getAmount(),
monthlyRentAmount: toCents(get(terms, 'monthlyRentAmount', 0)).getAmount(),
applicationFee: {
netAmount: toCents(get(terms, 'applicationFee.netAmount', 0)).getAmount(),
vatable: get(terms, 'applicationFee.vatable', false),
},
},
},
}
)
},
parties: 'parties',
invoiceTemplates: ({ invoiceTemplates }: any): $TSFixMe => {
return invoiceTemplates?.map((template: any) => ({
...template,
netAmount: toCents(get(template, 'netAmount', 0)).getAmount(),
...
}))
},
agents: 'agents',
leaseIdentifier: 'leaseIdentifier',
leaseTerms: 'leaseTerms',
settings: 'settings',
segments: ({ segments = [] }) => uniq(segments),
commission: ({ commission }: any) =>
commission && {
managementFee: transformCommissionFee(get(commission, 'managementFee')),
procurementFee: transformCommissionFee(get(commission, 'procurementFee')),
},
renewal: ({ renewal }: any) =>
renewal && {
newRentAmount: toCents(renewal.newRentAmount).getAmount(),
newCommission: {
managementFee: transformCommissionFee(get(renewal, 'newCommission.managementFee')),
procurementFee: transformCommissionFee(get(renewal, 'newCommission.procurementFee')),
},
depositTopUp: toCents(renewal.depositTopUp).getAmount(),
renewalFee: toCents(renewal.renewalFee).getAmount(),
leaseRenewsAt: renewal.leaseRenewsAt,
newLeaseTerms: renewal.newLeaseTerms,
},
terminatedReason: 'terminatedReason',
status: ({ status = 'draft' }) => toLower(status),
}

export const transformGetPortfolioResponse = (source: any) => morphism(getPortfolioResponseSchema, source)

Utils

For common operations, data manipulation etc.

Example

This example using Ramda.

export const getTenantAndLandlordIdsFromPortfolio = (portfolio: any) =>
pipe(
prop('parties'),
props(['tenants', 'owners']),
map(props(['primary', 'secondary'])),
flatten,
map(prop('partyId')),
filter((id: string) => !!id),
)(portfolio)

The same code in pure JS could be written like this (untested).

export const getTenantAndLandlordIdsFromPortfolio = (portfolio: any) => {
const { parties } = portfolio
const { tenants, owners } = parties
return [
tenants.primary.partyId,
...tenants.secondary.map(tenant => tenant.partyId),
owners.primary.partyId,
...owners.secondary.map(owner => owner.partyId),
].filter((id: string) => !!id)
}

Hooks

Custom hooks are for sharing reusable functionality between components. Refer to Building Custom Hooks for a visual guide what they are and how to create custom hooks.

Example

/**
* Ensures the portfolio and property is fetched and returns useful property fields
*
* @param {string} portfolioId Portfolio UUID
*/
export const usePortfolioProperty = (portfolioId: any): $TSFixMe => {
const dispatch = useDispatch()
const portfolio = useSelector(state => portfolioApiSelectors.getPortfolioById(state)(portfolioId))
const propertyId = path(['propertyId'], portfolio)
const propertyStreetAddress = useSelector(state => propertyApiSelectors.getPropertyStreetAddress(state)(propertyId))

useEffectOnce(() => {
if (!portfolio && portfolioId) {
dispatch(portfolioApiEvents.portfolio_request(portfolioId))
}
})

useEffect(() => {
if (!propertyStreetAddress && propertyId) {
dispatch(propertyApiEvents.property_request({ id: propertyId }))
}
}, [propertyStreetAddress, dispatch, propertyId])

return {
propertyId,
propertyStreetAddress: portfolio && propertyStreetAddress,
}
}

Index (module contract)

The index file serves as the modules public contract. Other modules interface with this module by importing from the contract. Other modules should not import directly from other files within the module. This allows the module to refactor easily without having an impact on the rest of the application.

Example

import * as portfolioApiConstants from './constants'
import { events as portfolioApiEvents, reducer as portfolioApiReducer } from './state'
import * as portfolioApiEpics from './epics'
import * as portfolioApiSelectors from './selectors'
import * as portfolioApiTransformers from './transformers'
import * as portfolioApiUtils from './utils'
import * as portfolioApiHooks from './hooks'

export {
portfolioApiConstants,
portfolioApiEpics,
portfolioApiEvents,
portfolioApiReducer,
portfolioApiSelectors,
portfolioApiTransformers,
portfolioApiUtils,
portfolioApiHooks,
}