The Lighthouse SDK is a data library built with Redux for communicating with the Lighthouse API. It abstracts API data communication, enabling reuse across multiple clients (Web, React Native), standardizes data structures for basic CRUD resources, and provides features such as offline support, optimistic updates, and state persistence.
The SDK uses Redux version 3.6.0, as specified in the package.json dependencies. This is a stable version of Redux that provides the core functionality for predictable state management.
For additional information about this version of Redux, refer to:
Key Redux concepts used in this SDK:
- Store: The single source of truth for application state
- Actions: Plain objects describing changes to state
- Reducers: Functions that specify how state changes in response to actions
- Middleware: Intercepts actions before they reach reducers
The Redux store in Lighthouse SDK is structured around a modular architecture. Each module represents a set of related functionality and data.
-
Store Configuration: The store is configured in
src/store/configure.js
, which sets up:- Redux middleware
- State persistence
- Offline support
- Initial state
-
Root Reducer: Combines all module reducers into a single reducer tree.
-
State Persistence: Uses
redux-persist
to save and retrieve state from local storage. -
Middleware Stack:
const middleware = composeEnhancers( applyMiddleware( thunk, // Handles async actions requestMiddleware, // Manages API requests trackingMiddleware, // Handles tracking events hookMiddleware, // Allows registering actions hooks messagesMiddleware, // Manages message registration/deregistration logsMiddleware, // Processes logging queue actionLoggerMiddleware // Logs Redux actions ) )
The store's state is organized into several key sections:
- Module States: Each module has its own slice of state
- Offline State: Manages offline queue and network status
- Authentication State: Stores authentication information
- Application State: Stores application context
- Version State: Tracks SDK version for migration handling
In the Lighthouse SDK, a "module" is a self-contained unit of functionality that includes:
- Action Creators: Functions that create and return action objects
- Reducers: Functions that update state based on dispatched actions
- Selectors: Functions that extract and derive data from state
- Middleware (optional): For module-specific side effects
Modules follow a standard structure:
export const actions = {...} // Action type constants
export const actionCreators = {...} // Functions that return action objects
export const selectors = {...} // Functions to extract data from state
export const reducer = {...} // Functions to update state
Modules are imported and used through the SDK's getModule
function:
import { getModule } from '@lighthouse/sdk'
const listId = 'all' // optional, defaults to 'default'
const location = getModule('location')
// In React components with Redux
function mapStateToProps(state) {
const locationSelectors = location.selectors(state)
return {
locations: locationSelectors(listId).list(),
currentLocation: locationSelectors.current(),
}
}
function mapDispatchToProps(dispatch) {
const { query, save, findById, remove } = location
return {
fetch: params => dispatch(query(listId, params)),
save: (params, payload, id) => dispatch(save(params, payload, id)),
findById: id => dispatch(findById(id)),
remove: id => dispatch(remove(id)),
}
}
To add a new CRUD module:
- Create a folder for the resource in
/modules
- Add the new module reducer in
/module/index.js
- Update the test for the root module
The SDK implements several state types to manage different aspects of data storage and manipulation:
The cache state is responsible for storing the actual data objects retrieved from or created for the API. Each entry in the cache contains:
- entity: The actual data object
- state: The current status of the data (resolved, resolving, save-failed, etc.)
- error: Any error information if applicable
- id: The unique identifier
- optimistic: Flag indicating if the entity exists only locally (optimistic update)
- originalEntity: Original data before optimistic updates (for rollback purposes)
States of cache items are defined in /src/constants/states.js
:
export const RESOLVING = 'resolving' // Requesting with server
export const RESOLVED = 'resolved' // Request complete
export const SAVE_FAILED = 'save-failed' // Saving failed
export const REMOVE_FAILED = 'remove-failed' // Removing failed
export const INVALIDATED = 'invalidated' // Data is stale
export const ROLLED_BACK = 'rolled-back' // Optimistic update failed
The list state manages collections of entities with:
- ids: Array of entity IDs in the collection
- params: The query parameters used to generate the list
- state: The loading state of the list
- error: Any error that occurred loading the list
- links: Pagination links for the list
- totalCount: Total number of items available
- lastUpdated: Timestamp of the last update
Lists are identified by a listId (defaults to 'default') allowing multiple lists of the same entity type.
The current state keeps track of the currently selected entity:
- id: The ID of the currently selected entity
This is used for tracking the active entity across UI contexts.
The Lighthouse SDK implements offline support using a custom version of redux-offline. The offline mode allows operations to continue working when the device is offline.
-
Optimistic Updates: When using the
optimistic: true
parameter, changes are immediately applied to the local state and the request is added to the offline queue. -
Action Queue: The
offline.outbox
in the Redux state maintains a queue of pending network requests. -
Network Detection: The SDK detects network status using browser/device APIs and updates
offline.online
state accordingly. -
Request Processing:
- When online, redux-offline processes its outbox queue sequentially
- Each request attempts to sync with the server
- Requests use a retry strategy with exponential backoff
-
Conflict Resolution:
- On success: The temporary ID is replaced with the server-provided ID
- On failure: Actions are marked as "rolled back" and can be retried
The offline functionality is configured in src/store/configure.js
:
const offlineConfig = {
batch,
detectNetwork: detectNetwork || defaultDetectNetwork,
discard: offlineDiscardFn,
effect: offlineEffectFn(requestOptions),
logger: offlineLogger || console,
persist: noop,
persistOptions: persistenceOpts,
retry: offlineRetryFn,
}
Key retry settings:
const offlineDecaySchedule = [
1000, // 1 second
1000 * 3, // 3 seconds
1000 * 8, // 8 seconds
]
// Optimistic update with offline support
const params = {
optimistic: true
}
const payload = {
body: 'Hi Friend!'
}
// Message will be available in cache immediately
// Request will be queued for when network is available
message.save(params, payload)
- Optimistic parameter in action creator creates a redux-offline action, assigning a temporary ID
- The request is added to the outbox queue
- When online, the queue is processed:
- On success: Entity is updated with database ID
- On failure: Entity is marked as "rolled-back" for retry
- Only save/update requests work offline
- GET requests require a network connection
- Previously fetched data can be used for offline operations but may be stale
- Network status is detected via browser/device APIs
- Non-optimistic requests use the request middleware
The SDK uses several middleware layers to handle different aspects of application logic:
Located in src/middleware/request.js
, this middleware:
- Handles all API requests that aren't optimistic updates
- Manages request lifecycle with request/success/failure actions
- Supports retry logic for failed requests
- Formats query parameters and request bodies
- Handles authentication errors
Located in src/middleware/messages/index.js
, this middleware:
- Manages the registration/deregistration of users for messaging
- Listens for application switching and authentication events
- Handles message delivery between applications
Located in src/modules/logs/middleware/index.js
, this middleware:
- Processes logs in the logging queue
- Throttles log sending to avoid flooding the server
- Ensures logs are sent when online
- Handles log failures and retries
Located in src/modules/tracking/middleware/index.js
, this middleware:
- Intercepts tracking actions
- Forwards tracking events to external tracking services
A standard middleware that allows action creators to return functions instead of action objects, enabling async logic.
Allows components to hook into actions and add side effects.
The SDK uses redux-persist
to save and rehydrate state to/from local storage:
Persistence is configured in src/store/configure.js
:
const persistenceOpts = Object.assign(
{},
defaultPersistenceOpts,
{
storage: storageAdapter,
},
)
The storageAdapter
is provided by the application and must implement the localStorage interface (getItem, setItem, removeItem).
- State Serialization: The Redux state is serialized and stored in local storage
- Debounced Writes: Changes are debounced (50ms default) to prevent excessive writes
- Hydration on Startup: When the application starts, state is retrieved from storage
- Version Checking: Major version changes trigger state reset to prevent errors
- Sanitization: Stored state is sanitized before use to fix offline state issues
The SDK handles state migration when the SDK version changes:
const hasMajorVersionChange = majorVersionChange(state)
if (hasMajorVersionChange) {
reduxLogger.warn('Major version change detected, resetting state...')
state = initialState
}
-
Optimistic Updates Conflicts:
- Local updates might conflict with server state
- Potential Solution: Implement proper conflict resolution in UI
-
Offline Queue Management:
- The outbox queue can grow large during extended offline periods
- Failed requests could block subsequent requests
- Potential Solution: Monitor queue size and implement manual queue management
-
Temporary IDs:
- Optimistic updates use temporary IDs that change after server sync
- Solution: Use the
current
selector which will handle the transition of temporary/permanent IDs
-
Rollback Handling:
- Failed optimistic updates become "rolled-back" but still exist in cache
- Solution: There are manual retry actions in the mobile app
Enable Redux Logger for detailed action logs:
const config = {
reduxLoggerPredicate: () => true, // Enable logging all actions
}
- Check
offline.online
state to verify network detection - Inspect
offline.outbox
for pending requests - Review retry configuration if requests fail repeatedly
- Inspect cache state for entities with
state: 'rolled-back'
- Look for optimistic updates that might have failed
- Check for entities with temporary IDs (typically CUID format)
- Verify the storage adapter implementation
- Check for localStorage quota limits
- Consider clearing persisted state during major version upgrades
- Authentication errors: Re-authenticate the user
- Network failures: Implement appropriate retry UI
- Validation errors: Show detailed field-level errors from server
- Stale data: Implement refresh patterns for invalidated data