State management
Application state for workspaces, views, and widgets lives in a single React
Context: WorkspacesContext. Components interact with it through the
useWorkspaces() hook.
- Provider: src/workspaces/state/WorkspacesContext.tsx
- Hook:
useWorkspaces()— throws if used outsideWorkspacesProvider.
The context object itself, the useWorkspaces() hook, and the
WorkspacesContextValue type live in a sibling
src/workspaces/state/workspaces-context.ts
module (re-exported from WorkspacesContext.tsx). Keeping the
createContext call out of the provider's .tsx file gives the context a
stable identity across React Fast Refresh, so editing the provider during
development no longer throws "used outside provider". Import paths are
unchanged — continue importing from @/workspaces/state/WorkspacesContext.
What the provider owns
WorkspacesProvider:
- Is mounted inside the auth provider and keyed by
user.id, so switching users (or ArcGIS connections) gives a fresh, isolated state. - Loads the workspace list when the user changes, and loads an individual
workspace on demand via
setActiveWorkspace. - Injects the live ArcGIS portal into the persistence provider
(
workspacePersistence.setPortal(portal)). - Runs a debounced auto-save whenever workspace state is mutated.
The context value
useWorkspaces() returns a WorkspacesContextValue. The most important members:
State
| Member | Type | Description |
|---|---|---|
loading | boolean | Whether a workspace list or workspace is loading. |
mode | WorkspaceMode | "edit" or "view". |
workspaces | WorkspaceSummary[] | Lightweight list for the index and switcher. |
activeWorkspace | WorkspaceConfig | null | The fully loaded active workspace. |
activeViewId | string | null | The active tab's id. |
activeView | ViewConfig | null | The active tab. |
selectedWidgetId | string | null | The single widget selected for configuration (set only when exactly one is selected). |
selectedWidgetIds | string[] | All widgets in the current multi-selection (used for grouping). |
saveStatus | "idle" | "dirty" | "saving" | "saved" | "error" | Drives the save indicator. |
hasUnsavedChanges | boolean | True when saveStatus is "dirty" or "error". Enables the Save button and arms the unsaved-changes guards. |
error | string | null | The last error message, if any. |
Workspace operations
| Method | Description |
|---|---|
createWorkspace(name) | Create and persist a new workspace. |
updateWorkspace(id, patch) | Patch name / description. |
deleteWorkspace(id) | Delete a workspace. |
setActiveWorkspace(id) | Load and activate a workspace. |
persistWorkspace() | Save the active workspace; resolves true on success, false on failure/no-op. |
View operations
| Method | Description |
|---|---|
createView(name) | Add a new tab. |
updateView(id, patch) | Patch a view's name / description. |
deleteView(id) | Remove a tab. |
duplicateView(id) | Deep-copy a tab with fresh ids. |
reorderView(from, to) | Reorder tabs. |
setActiveView(id) | Switch the active tab. |
Widget operations
| Method | Description |
|---|---|
addWidgetToView(viewId, widgetType, targetGroupId?) | Add a widget instance from the registry; with targetGroupId it is added straight into that group as a new tab. |
updateWidgetConfig(viewId, widgetId, config) | Update a widget's saved config. |
removeWidgetFromView(viewId, widgetId) | Remove a widget (also detaches it from any group). |
mutateView(viewId, updater) | Low-level escape hatch for layout updates. |
Widget group operations
Groups combine two or more widgets into one tabbed tile; see Widget groups.
| Method | Description |
|---|---|
groupWidgets(viewId, widgetIds) | Combine two or more widgets into a new tabbed group. |
ungroupGroup(viewId, groupId) | Dissolve a group back into top-level widgets. |
addWidgetToGroup(viewId, groupId, widgetId) | Add an existing widget to a group as the active tab. |
removeWidgetFromGroup(viewId, groupId, widgetId) | Pop a member out to a top-level widget. |
reorderGroupMembers(viewId, groupId, from, to) | Reorder a group's tabs. |
setActiveGroupMember(viewId, groupId, memberId) | Choose which tab is shown. |
updateGroup(viewId, groupId, patch) | Patch a group's title / hideHeader. |
UI state
| Method | Description |
|---|---|
setSelectedWidgetId(id | null) | Select a single widget for the config drawer. |
toggleWidgetSelection(id) | Add/remove a widget from the multi-selection (modifier-click). |
clearWidgetSelection() | Clear the multi-selection. |
setMode(mode) | Switch between edit and view. |
clearError() | Dismiss the current error. |
Save lifecycle
Saving is explicit: any mutation marks the active workspace dirty, and the
user persists by clicking Save in the header. The button is enabled only
while there are unsaved changes (hasUnsavedChanges — i.e. dirty or error),
so a freshly opened or already-saved workspace shows a disabled Save. The
indicator renders directly from saveStatus (Saving… / Saved /
Retry save).
Protecting unsaved changes
Because saving is manual, any interaction that would leave edit mode or drop the
in-memory workspace is routed through the unsaved-changes guard
(useUnsavedChangesGuard() / UnsavedChangesGuardProvider). When
hasUnsavedChanges is true the guard prompts the user to Save changes,
Continue without saving, or Cancel before the action runs; when there
are no unsaved changes the action runs immediately. The guard wraps:
- Go Live (leaving edit mode for the live view).
- Sign out, switch workspaces, All workspaces, New workspace, and switch connections in the user menu.
A beforeunload listener is armed while hasUnsavedChanges is true, so closing
or reloading the tab triggers the browser's native confirmation as a catch-all
safety net.
Read next
- Workspaces & views — the model and the user-facing workflows.
- Persistence — the storage provider interface.