Skip to content
Annie Wang edited this page May 5, 2021 · 7 revisions

Audience: Developer

This guide covers the structure and workflows of our Redux store.

Redux store structure

We divide the store into the following slices:

  • userData
  • siteData
  • customerData
  • inventoryData

userData

  • isLoading: TODO figure out whether the application is loading ***
  • lastUpdated: date and time when user data was last updated. This is only updated on the initial login (through refreshUserData called in loginUser).
  • isOnline: a boolean for whether or not the user is connected to a network. This is updated through the checkOnline function in userData. Read more about offline workflows here. TODO ADD LINK
  • users: an entity adapter for all users linked to the current site.
  • currentUserId: the id of the current user (user who is signed in).

Relevant PRs

inventoryData

  • products is the list of all products pulled from Airtable (not filtered by site).
  • sitesInventory: map between site ids and SiteInventoryData, which is an interface in inventoryDataSlice.ts with 3 entity adapters:
    • siteInventory: all inventory related to the site
    • purchaseRequests: all purchase requests related to inventory from the site
    • inventoryUpdates: all inventory updates related to inventory from the site
  • currentInventoryId: id of the inventory record, used for viewing individual inventory profile pages through InventoryProfile screen (/inventory/item)
  • currentPurchaseRequestId: id referencing the 'current' purchase request, which is used for viewing individual purchase requests on the PurchaseRequest screen (inventory/purchase-requests/purchase-request)

Relevant PRs

customerData

  • sitesCustomers: map between site ids and SiteCustomerData, which is a an interface in customerDataSlice.ts with 3 entity adapters:
    • customers: all customers related to the site
    • payments: all payments linked to customers from the site
    • meterReadings: all meter readings linked to customers from the site
  • currentCustomerId: id referencing the 'current' customer, which is used for viewing individual customer profiles through the CustomerProfile (customers/customer)

Relevant PRs

siteData

  • isLoading: TODO figure out
  • currentSiteId: id of the site that the user is currently viewing (which can be toggled using the dropdown on the Home screen (/home)
  • sites: a map between site ids and site records, which is limited to the sites that the user has access to. Note that this is not an entity adapter.

Relevant PRs

Retrieving data from the store using selectors

We use selectors and hooks to access data from the Redux store. Redux recommends using the React-Redux hooks API as the default approach over the connect API, as it is "simpler and works better with TypeSript" (Redux Docs)

Each slice listed in the previous section has several selectors dedicated to retrieving various data from the store.

Custom selectors

For data based on entity adapters, we import a set of default selectors through getSelectors (see Redux docs).

// inventoryDataSlice.ts
export const {
  selectEntities: selectAllCurrentSiteInventory,
  selectAll: selectAllCurrentSiteInventoryArray,
  selectById: selectCurrentSiteInventoryById,
  selectIds: selectCurrentSiteInventoryIds,
} = siteInventoryAdapter.getSelectors(
  (state: RootState) => state.inventoryData.sitesInventory[state.siteData.currentSiteId].siteInventory,
);

We can use these selectors to build other selectors using createSelector (learn more in Redux docs), which maximizes the performance benefits of using selectors since selectors are designed so that a selector is not recomputed unless one of its arguments changes. Here are some example applications of custom selectors based on the data in current___Id values in the store.

// inventoryData.ts

// -- Custom selectors for current inventory and purchase request --

// Select the value for currentInventoryId directly from the store
export const selectCurrentInventoryId = (state: RootState): string => state.inventoryData.currentInventoryId;

// Select the value for currentPurchaseRequestId directly from the store
export const selectCurrentPurchaseRequestId = (state: RootState): string =>
  state.inventoryData.currentPurchaseRequestId;

// Select the inventory record corresponding to the current inventory item (based on currentInventoryId)
export const selectCurrentInventory = createSelector(
  selectCurrentInventoryId,
  store.getState,
  (currentInventoryId, state) => selectCurrentSiteInventoryById(state, currentInventoryId),
);

// Select the product corresponding to the current inventory item (based on currentInventoryId)
export const selectCurrentInventoryProduct = createSelector(
  selectCurrentInventoryId,
  store.getState,
  (inventoryId, state) => selectProductByInventoryId(state, inventoryId),
);

Usage Example

// CreatePurchaseRequest.tsx

...
import { useSelector } from 'react-redux';
import { selectCurrentInventoryProduct } from '../../lib/redux/inventoryData';
...
const product = useSelector(selectCurrentInventoryProduct);
...

Selectors with arguments

Sometimes we need selectors that can take in arguments. For example, the following selector is used to retrieve a product based on an inventory id parameter.

// inventoryData.ts

const getInventoryId = (_: RootState, inventoryId: string) => inventoryId;
...
export const selectProductByInventoryId = createSelector(getInventoryId, store.getState, (inventoryId, state) =>
  selectProductById(state, selectProductIdByInventoryId(state, inventoryId) || ''),
);

Usage example:

// PurchaseRequest.tsx

import { useSelector } from 'react-redux';
import { RootState } from '../../lib/redux/store';
import { EMPTY_PRODUCT } from '../../lib/redux/inventoryDataSlice';
import { selectProductByInventoryId } from '../../lib/redux/inventoryData';

...
const product = useSelector((state: RootState) => selectProductByInventoryId(state, purchaseRequest.inventoryId)) || EMPTY_PRODUCT;

Helpful links

Clone this wiki locally