Skip to main content
This page lives in the Developers section and is also referenced from Deployment. If you arrived from there, your sidebar has switched to Developers.

State Management

CDT uses a custom middleware pattern built on React Context and useReducer. There is no external state library — each domain owns its own context, reducer, and action types. All contexts are composed into a single AppProvider that wraps the application once.

Pattern

Each store slice follows the same four-part structure:

1. State type

Defines the shape of the slice:

export type BuildingsState = {
buildings: Building[]
building: Building | null
}

2. Action types

A discriminated union of all actions the reducer handles:

export type BuildingsActions =
| { type: 'SET_BUILDINGS'; payload: Building[] }
| { type: 'SET_BUILDING'; payload: Building | null }

3. Reducer

Pure function — receives the current state and an action, returns the next state. Components never modify state directly.

export const BuildingsReducer = (
state: BuildingsState,
action: BuildingsActions
): BuildingsState => {
switch (action.type) {
case 'SET_BUILDINGS':
return { ...state, buildings: action.payload }
case 'SET_BUILDING':
return { ...state, building: action.payload }
default:
return state
}
}

4. Context and Provider

The context exposes state and dispatch. The provider wraps useReducer and passes both down:

export const BuildingsContext = React.createContext<{
state: InitialStateType
dispatch: React.Dispatch<BuildingsActions>
}>({ state: initialState, dispatch: () => null })

export const BuildingsProvider: React.FC<React.PropsWithChildren> = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, initialState)
return (
<BuildingsContext.Provider value={{ state, dispatch }}>
{children}
</BuildingsContext.Provider>
)
}

Consuming a context

Import the context and destructure state and dispatch with useContext:

import { MapStyleContext } from '@collabdt/core/store/Map/context'

export default function MapStyleMenu() {
const { state, dispatch } = useContext(MapStyleContext)

return (
<button onClick={() => dispatch({ type: 'UPDATE_MAP_STYLE', payload: { name: 'streets' } })}>
Streets
</button>
)
}

Components dispatch actions — they never calculate the next state themselves.

CombineProviders

All providers are composed into a single AppProvider using a compose utility in @collabdt/core/store/CombineProviders.tsx. This avoids deeply nested JSX while keeping each provider independent.

const compose = providers =>
providers.reduce(
(Prev, Curr) =>
function ComposedProvider({ children }) {
return <Prev><Curr>{children}</Curr></Prev>
}
)

export const AppProvider = compose([
AppConfigProvider,
BimProvider,
MapProvider,
MenusProvider,
ToolsProvider,
ContentProvider,
DatasetsProvider,
FilesProvider,
BuildingsProvider,
PointCloudProvider,
PermissionsProvider,
])

AppProvider is rendered once at the application root. All child components have access to every context without prop drilling.

Providers

ProviderContext keyPurpose
AppConfigProviderappConfigActive organization and current user
BimProviderbimIFC components, fragments, world, floor plans, BCF topics, model UI state
MapProvidermapMap instance, style, camera position, layers, click/hover managers, GeoJSON
MenusProvidermenusActive viewer, selected tab, sidebar panels, rows per page, visible sensors and comments
ToolsProvidertoolsCurrently active tool ID
ContentProvidercurrentContentActive content view (map, sites, buildings, files, etc.) and instance
DatasetsProviderdatasetsDataset list, selected dataset, added datasets
FilesProviderfilesFile list, map file IDs, file manager open state, editing file
BuildingsProviderbuildingsBuilding list, selected building
PointCloudProviderpointcloudPoint cloud viewer state
PermissionsProviderabilityCASL ability object built from the user's role permissions

Key files

FileRole
@collabdt/core/store/CombineProviders.tsxComposes all providers into AppProvider
@collabdt/core/store/ActionMap.tsShared utility type for mapping action payloads
@collabdt/core/store/<Domain>/context.tsxContext definition and provider for each slice
@collabdt/core/store/<Domain>/reducer.tsState type, action types, and reducer for each slice
@collabdt/core/store/index.tsRe-exports

Design decisions

  • No external library. React Context + useReducer covers CDT's needs without the overhead of Redux or Zustand. The pattern is intentionally close to Redux (actions, reducers, dispatch) so it's familiar and easy to reason about.
  • One reducer per domain. Each slice is fully self-contained. Adding a new domain means creating a context.tsx and reducer.ts, then registering the provider in CombineProviders.tsx.
  • PermissionsProvider is special. It does not use useReducer — it reads the user's session via NextAuth and builds a CASL MongoAbility object from the fetched role permissions. It is memoized so the ability only rebuilds when permissions change.
  • ContentProvider is special. It uses useState instead of useReducer because its state transitions are simple and don't benefit from the action/reducer pattern.