State Management
The@bitwarden/state library provides a comprehensive State Provider Framework for centralized application state management across Bitwarden clients.
Overview
The State Provider Framework was designed to:- Enable domain ownership - Teams own their state definitions
- Enforce best practices - Reduce boilerplate and prevent common mistakes
- Support account switching - Built-in multi-account support
- Provide trustworthy observables - Reliable reactive state streams
- Simplify testing - Comprehensive fake/mock implementations
Core Concepts
State Storage Locations
State can be stored in two primary locations:- Disk (
"disk") - Persistent storage (survives app restarts) - Memory (
"memory") - In-memory cache (cleared on app restart)
- Web:
"disk"defaults to session storage,"disk-local"for local storage - Desktop/Browser: Platform-specific persistent storage
State Scopes
- Global State - Application-wide state (not user-specific)
- User State - State scoped to individual users
- Active User State - State for currently active user (deprecated)
- Derived State - Computed state based on other state
State Definitions
StateDefinition
StateDefinition defines a storage location and top-level namespace.
Location: Teams add entries to a central state-definitions.ts file
- Use camelCase for state names
- Names must be unique per storage location
- Same name can be used for both disk and memory
- Never change
StateDefinitionnames for disk storage without migration
KeyDefinition and UserKeyDefinition
KeyDefinition and UserKeyDefinition specify individual state elements.
UserKeyDefinition (User-Scoped State)
KeyDefinition (Global State)
Complex State with Deserializers
Array and Record Helpers
Key Definition Options
| Option | Required | Description |
|---|---|---|
deserializer | Yes | Converts JSON to typed object |
clearOn | Yes (UserKeyDefinition) | When to clear state: ["logout"], ["lock"], both, or [] |
cleanupDelayMs | No | Delay before cleanup after last unsubscribe (default: 1000ms) |
State Provider
StateProvider is the main service for accessing state.
Getting State
Alternative: Specific Providers
For lighter dependencies, inject specific providers:Working with State
GlobalState<T>
SingleUserState<T>
Reading State
Updating State
Update Options
shouldUpdate: Prevent Unnecessary Updates
combineLatestWith: Conditional Updates
Derived State
Derived state caches expensive computations based on other state.DeriveDefinition
Using Derived State
Force Derived Value
Useful for clearing derived state during logout:State Migrations
Migrate data when changing state definitions or structure.Creating a Migration
Location:libs/state/src/state-migrations/migrations/
Migration Best Practices
- Never skip migrations - Run all migrations in order
- Test thoroughly - Migrations are irreversible
- Use KeyDefinitionLike - Avoid importing from application code
- Handle null data - Users may not have data to migrate
Why Not ActiveUserState?
ActiveUserState is deprecated due to race condition issues.
Problem: Account Switching Race Condition
Solution: Use SingleUserState
Benefits of SingleUserState
- No race conditions - Operations always target same user
- Flexible API - Can query any user’s data
- Better account switching - Clean transitions with
switchMap
Testing
Fake State Provider
Best Practices
State Definition
-
Choose appropriate storage:
- Disk for persistent data
- Memory for caches/computed data
-
Set proper
clearOnevents:["logout"]for sensitive data["lock", "logout"]for very sensitive data[]for settings that survive logout
-
Write good deserializers:
- Handle
nullandundefined - Convert dates/complex types correctly
- Test edge cases
- Handle
State Updates
-
Use
shouldUpdatewhen possible:- Reduces unnecessary I/O
- Prevents redundant observable emissions
-
Avoid
firstValueFrom()after updates:- Use return value from
update() - Or stay in reactive observable world
- Use return value from
-
Handle
nullin update functions:- State can be
nullorundefined - Provide defaults when needed
- State can be
Observable Patterns
-
Don’t leave reactivity:
- Propagate observables through your app
- Use
asyncpipe in templates - Avoid premature subscription
-
Clean account switching:
- Use
switchMapwithactiveAccount$ - Combine streams with same user ID
- Use
Related Libraries
- @bitwarden/state-internal - Internal state implementation (do not use directly)
- @bitwarden/state-test-utils - Testing utilities
- @bitwarden/storage-core - Storage service implementations
- Platform Library - Storage abstractions
State Architecture Diagram
Seelibs/state/state_diagram.svg for visual architecture overview.
Source Code
- State Library:
libs/state/ - State Internal:
libs/state-internal/ - State Migrations:
libs/state/src/state-migrations/migrations/ - Storage Core:
libs/storage-core/ - Test Utils:
libs/state-test-utils/