DeviceListProvider
A React Context provider for managing medical device list state across the application with centralized state management and real-time updates.
Overview
The DeviceListProvider component provides a centralized state management solution for medical device lists using React Context. It manages the list of available devices, their connection status, and provides a consistent API for updating device information across components.
Context API
DeviceListContext
interface DeviceListContext {
deviceListItems: DeviceListItem[];
setDeviceListItems: React.Dispatch<React.SetStateAction<DeviceListItem[]>>;
}
useDeviceList Hook
export const useDeviceList = () => {
const context = React.useContext(DeviceListContext);
if (context === undefined) {
throw new Error(
"useDeviceList must be used within a DeviceListContextProvider",
);
}
return context;
};
Features
🏥 Centralized State Management
- Global Device State: Single source of truth for device list across the app
- Real-time Updates: Live synchronization of device status changes
- Type Safety: Full TypeScript support with comprehensive type definitions
🔄 State Synchronization
- BT Management Integration: Seamless integration with Bluetooth device management
- Status Updates: Automatic status synchronization across all components
- Performance Optimized: Efficient state updates with minimal re-renders
🎯 Developer Experience
- Hook-based API: Simple hook for accessing device list state
- Error Boundaries: Built-in error handling for context usage
- TypeScript Support: Full type safety with IntelliSense support
Basic Usage
Simple Provider Setup
import React from "react";
import { DeviceListProvider, BTDeviceList } from "@modules/bt-device";
const App = () => {
return (
<DeviceListProvider>
<BTDeviceList
itemProps={{
onPress: (device) => console.log("Selected:", device.deviceType),
}}
/>
</DeviceListProvider>
);
};
Multiple Components
import React from "react";
import {
DeviceListProvider,
BTDeviceList,
useDeviceList,
} from "@modules/bt-device";
const DeviceCounter = () => {
const { deviceListItems } = useDeviceList();
return <Text>Total Devices: {deviceListItems.length}</Text>;
};
const DeviceApp = () => {
return (
<DeviceListProvider>
<View style={{ flex: 1 }}>
<DeviceCounter />
<BTDeviceList />
</View>
</DeviceListProvider>
);
};
Advanced Usage
Custom State Management
import React, { useEffect } from "react";
import { DeviceListProvider, useDeviceList } from "@modules/bt-device";
import { DeviceStatus, IntegratedDevices } from "@modules/bt-management";
const CustomDeviceManager = () => {
const { deviceListItems, setDeviceListItems } = useDeviceList();
// Add custom device
const addCustomDevice = () => {
const newDevice = {
title: "Custom Device",
subtitle: "User Added Device",
deviceImageUri: "custom-image-uri",
status: DeviceStatus.DISCONNECTED,
deviceType: IntegratedDevices.BGM,
};
setDeviceListItems((prev) => [...prev, newDevice]);
};
// Update device status
const updateDeviceStatus = (
deviceType: IntegratedDevices,
status: DeviceStatus,
) => {
setDeviceListItems((prev) =>
prev.map((item) =>
item.deviceType === deviceType ? { ...item, status } : item,
),
);
};
// Remove device
const removeDevice = (deviceType: IntegratedDevices) => {
setDeviceListItems((prev) =>
prev.filter((item) => item.deviceType !== deviceType),
);
};
return (
<View>
<Button title="Add Custom Device" onPress={addCustomDevice} />
<Button
title="Connect First Device"
onPress={() =>
updateDeviceStatus(
deviceListItems[0]?.deviceType,
DeviceStatus.CONNECTED,
)
}
/>
<BTDeviceList />
</View>
);
};
const App = () => (
<DeviceListProvider>
<CustomDeviceManager />
</DeviceListProvider>
);
Integration with BT Management
import React, { useCallback } from "react";
import { BTProvider, DeviceStatus, DeviceData } from "@modules/bt-management";
import { DeviceListProvider, useDeviceList } from "@modules/bt-device";
const BTIntegratedApp = () => {
const { setDeviceListItems } = useDeviceList();
const handleDeviceStatusUpdate = useCallback(
(
status: DeviceStatus,
device: DeviceData,
measurementType?: MeasurementTypeKey,
) => {
setDeviceListItems((prev) =>
prev.map((item) =>
item.deviceType === device.name ? { ...item, status } : item,
),
);
},
[setDeviceListItems],
);
const handleDeviceFound = useCallback(
(bleDevice: IBaseDevice, scanInstance: ScanManagerImplementation) => {
// Update device list with discovered device
setDeviceListItems((prev) =>
prev.map((item) =>
item.deviceType === bleDevice.deviceName
? { ...item, status: DeviceStatus.CONNECTED }
: item,
),
);
},
[setDeviceListItems],
);
return (
<BTProvider
bleManager={bleManager}
onDeviceStatusUpdated={handleDeviceStatusUpdate}
onDeviceFound={handleDeviceFound}
onScanError={(error) => console.error("Scan error:", error)}
onDeviceErrors={(error, device) => console.error("Device error:", error)}
onMeasurementUpdated={(measurement, device) => {
console.log("New measurement:", measurement);
}}
onStreamData={(measurement, device) => {
console.log("Stream data:", measurement);
}}
useFocusEffect={useFocusEffect}
>
<BTDeviceList />
</BTProvider>
);
};
const App = () => (
<DeviceListProvider>
<BTIntegratedApp />
</DeviceListProvider>
);
Filtered Device Views
import React, { useMemo } from "react";
import { DeviceListProvider, useDeviceList } from "@modules/bt-device";
import { DeviceStatus } from "@modules/bt-management";
const ConnectedDevices = () => {
const { deviceListItems } = useDeviceList();
const connectedDevices = useMemo(
() =>
deviceListItems.filter(
(device) => device.status === DeviceStatus.CONNECTED,
),
[deviceListItems],
);
return (
<View>
<Text style={styles.title}>
Connected Devices ({connectedDevices.length})
</Text>
{connectedDevices.map((device, index) => (
<DeviceCard key={index} device={device} />
))}
</View>
);
};
const DisconnectedDevices = () => {
const { deviceListItems } = useDeviceList();
const disconnectedDevices = useMemo(
() =>
deviceListItems.filter(
(device) => device.status === DeviceStatus.DISCONNECTED,
),
[deviceListItems],
);
return (
<View>
<Text style={styles.title}>
Available Devices ({disconnectedDevices.length})
</Text>
<BTDeviceList data={disconnectedDevices} />
</View>
);
};
const DeviceDashboard = () => (
<DeviceListProvider>
<ScrollView>
<ConnectedDevices />
<DisconnectedDevices />
</ScrollView>
</DeviceListProvider>
);
Persistent State
import React, { useEffect } from "react";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { DeviceListProvider, useDeviceList } from "@modules/bt-device";
const STORAGE_KEY = "device_list_state";
const PersistentDeviceProvider = ({ children }) => {
const { deviceListItems, setDeviceListItems } = useDeviceList();
// Load state from storage
useEffect(() => {
const loadState = async () => {
try {
const savedState = await AsyncStorage.getItem(STORAGE_KEY);
if (savedState) {
const parsedState = JSON.parse(savedState);
setDeviceListItems(parsedState);
}
} catch (error) {
console.error("Failed to load device state:", error);
}
};
loadState();
}, [setDeviceListItems]);
// Save state to storage
useEffect(() => {
const saveState = async () => {
try {
await AsyncStorage.setItem(
STORAGE_KEY,
JSON.stringify(deviceListItems),
);
} catch (error) {
console.error("Failed to save device state:", error);
}
};
saveState();
}, [deviceListItems]);
return <>{children}</>;
};
const App = () => (
<DeviceListProvider>
<PersistentDeviceProvider>
<BTDeviceList />
</PersistentDeviceProvider>
</DeviceListProvider>
);
State Management Patterns
Optimistic Updates
const OptimisticDeviceUpdates = () => {
const { setDeviceListItems } = useDeviceList();
const connectDevice = async (deviceType: IntegratedDevices) => {
// Optimistic update
setDeviceListItems((prev) =>
prev.map((item) =>
item.deviceType === deviceType
? { ...item, status: DeviceStatus.MEASURING }
: item,
),
);
try {
// Attempt actual connection
await actualConnectDevice(deviceType);
// Update to connected
setDeviceListItems((prev) =>
prev.map((item) =>
item.deviceType === deviceType
? { ...item, status: DeviceStatus.CONNECTED }
: item,
),
);
} catch (error) {
// Revert on failure
setDeviceListItems((prev) =>
prev.map((item) =>
item.deviceType === deviceType
? { ...item, status: DeviceStatus.DISCONNECTED }
: item,
),
);
console.error("Connection failed:", error);
}
};
return (
<BTDeviceList
itemProps={{
onPress: (device) => connectDevice(device.deviceType),
}}
/>
);
};
Batch Updates
const BatchDeviceUpdates = () => {
const { setDeviceListItems } = useDeviceList();
const updateMultipleDevices = (
updates: Array<{
deviceType: IntegratedDevices;
status: DeviceStatus;
}>,
) => {
setDeviceListItems((prev) =>
prev.map((item) => {
const update = updates.find((u) => u.deviceType === item.deviceType);
return update ? { ...item, status: update.status } : item;
}),
);
};
const connectAllDevices = () => {
updateMultipleDevices([
{ deviceType: IntegratedDevices.BP2, status: DeviceStatus.CONNECTED },
{ deviceType: IntegratedDevices.SPO2, status: DeviceStatus.CONNECTED },
{ deviceType: IntegratedDevices.F4, status: DeviceStatus.CONNECTED },
]);
};
return (
<View>
<Button title="Connect All" onPress={connectAllDevices} />
<BTDeviceList />
</View>
);
};
State Validation
const ValidatedDeviceProvider = ({ children }) => {
const { deviceListItems, setDeviceListItems } = useDeviceList();
const validateAndSetDevices = useCallback(
(newItems: DeviceListItem[]) => {
// Validate device items
const validItems = newItems.filter((item) => {
return (
item.deviceType &&
Object.values(IntegratedDevices).includes(item.deviceType) &&
Object.values(DeviceStatus).includes(item.status)
);
});
if (validItems.length !== newItems.length) {
console.warn("Some device items were filtered out due to validation");
}
setDeviceListItems(validItems);
},
[setDeviceListItems],
);
return (
<DeviceValidationContext.Provider value={{ validateAndSetDevices }}>
{children}
</DeviceValidationContext.Provider>
);
};
Performance Optimization
Memoized Selectors
import { useMemo } from "react";
const useDeviceSelectors = () => {
const { deviceListItems } = useDeviceList();
const selectors = useMemo(
() => ({
connectedDevices: deviceListItems.filter(
(d) => d.status === DeviceStatus.CONNECTED,
),
disconnectedDevices: deviceListItems.filter(
(d) => d.status === DeviceStatus.DISCONNECTED,
),
measuringDevices: deviceListItems.filter(
(d) => d.status === DeviceStatus.MEASURING,
),
lowBatteryDevices: deviceListItems.filter(
(d) => d.status === DeviceStatus.LOW_BATTERY,
),
deviceCount: deviceListItems.length,
connectedCount: deviceListItems.filter(
(d) => d.status === DeviceStatus.CONNECTED,
).length,
}),
[deviceListItems],
);
return selectors;
};
// Usage
const DeviceStats = () => {
const { connectedCount, deviceCount } = useDeviceSelectors();
return (
<Text>
Connected: {connectedCount}/{deviceCount}
</Text>
);
};
Debounced Updates
import { useCallback, useRef } from "react";
const useDebouncedDeviceUpdate = (delay = 300) => {
const { setDeviceListItems } = useDeviceList();
const timeoutRef = useRef<NodeJS.Timeout>();
const debouncedUpdate = useCallback(
(updater: (prev: DeviceListItem[]) => DeviceListItem[]) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
setDeviceListItems(updater);
}, delay);
},
[setDeviceListItems, delay],
);
return debouncedUpdate;
};
// Usage for rapid status updates
const RapidStatusUpdates = () => {
const debouncedUpdate = useDebouncedDeviceUpdate(500);
const handleStatusChange = (
deviceType: IntegratedDevices,
status: DeviceStatus,
) => {
debouncedUpdate((prev) =>
prev.map((item) =>
item.deviceType === deviceType ? { ...item, status } : item,
),
);
};
return <BTDeviceList /* ... */ />;
};
Error Handling
Context Error Boundary
import React, { Component } from "react";
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
}
class DeviceListErrorBoundary extends Component<
React.PropsWithChildren<{}>,
ErrorBoundaryState
> {
constructor(props: React.PropsWithChildren<{}>) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error("DeviceList error:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<View style={styles.errorContainer}>
<Text style={styles.errorTitle}>Device List Error</Text>
<Text style={styles.errorMessage}>
Failed to load device list. Please try again.
</Text>
<Button
title="Reload"
onPress={() => this.setState({ hasError: false })}
/>
</View>
);
}
return this.props.children;
}
}
// Usage
const App = () => (
<DeviceListErrorBoundary>
<DeviceListProvider>
<BTDeviceList />
</DeviceListProvider>
</DeviceListErrorBoundary>
);
Safe State Updates
const SafeDeviceUpdates = () => {
const { setDeviceListItems } = useDeviceList();
const safeUpdateDevice = useCallback(
(deviceType: IntegratedDevices, updates: Partial<DeviceListItem>) => {
try {
setDeviceListItems((prev) => {
const deviceIndex = prev.findIndex(
(item) => item.deviceType === deviceType,
);
if (deviceIndex === -1) {
console.warn(`Device ${deviceType} not found in list`);
return prev;
}
const newItems = [...prev];
newItems[deviceIndex] = { ...newItems[deviceIndex], ...updates };
return newItems;
});
} catch (error) {
console.error("Failed to update device:", error);
}
},
[setDeviceListItems],
);
return { safeUpdateDevice };
};
Testing
Provider Testing
import React from "react";
import { render, act } from "@testing-library/react-native";
import { DeviceListProvider, useDeviceList } from "@modules/bt-device";
const TestComponent = () => {
const { deviceListItems, setDeviceListItems } = useDeviceList();
return (
<View>
<Text testID="device-count">{deviceListItems.length}</Text>
<Button
testID="add-device"
title="Add Device"
onPress={() => setDeviceListItems((prev) => [...prev, mockDevice])}
/>
</View>
);
};
describe("DeviceListProvider", () => {
it("provides initial device list", () => {
const { getByTestId } = render(
<DeviceListProvider>
<TestComponent />
</DeviceListProvider>,
);
expect(getByTestId("device-count").props.children).toBeGreaterThan(0);
});
it("updates device list state", () => {
const { getByTestId } = render(
<DeviceListProvider>
<TestComponent />
</DeviceListProvider>,
);
const initialCount = Number(getByTestId("device-count").props.children);
act(() => {
fireEvent.press(getByTestId("add-device"));
});
expect(Number(getByTestId("device-count").props.children)).toBe(
initialCount + 1,
);
});
});
Hook Testing
import { renderHook, act } from "@testing-library/react-hooks";
import { DeviceListProvider, useDeviceList } from "@modules/bt-device";
describe("useDeviceList", () => {
const wrapper = ({ children }) => (
<DeviceListProvider>{children}</DeviceListProvider>
);
it("throws error when used outside provider", () => {
const { result } = renderHook(() => useDeviceList());
expect(result.error).toEqual(
Error("useDeviceList must be used within a DeviceListContextProvider"),
);
});
it("provides device list context", () => {
const { result } = renderHook(() => useDeviceList(), { wrapper });
expect(result.current.deviceListItems).toEqual(expect.any(Array));
expect(result.current.setDeviceListItems).toEqual(expect.any(Function));
});
it("updates device list through context", () => {
const { result } = renderHook(() => useDeviceList(), { wrapper });
act(() => {
result.current.setDeviceListItems([]);
});
expect(result.current.deviceListItems).toEqual([]);
});
});
Integration Testing
import React from "react";
import { render, fireEvent } from "@testing-library/react-native";
import { DeviceListProvider, BTDeviceList } from "@modules/bt-device";
describe("DeviceListProvider Integration", () => {
it("integrates with BTDeviceList", () => {
const mockOnPress = jest.fn();
const { getByTestId } = render(
<DeviceListProvider>
<BTDeviceList itemProps={{ onPress: mockOnPress }} />
</DeviceListProvider>,
);
const firstDevice = getByTestId("device-item-0");
fireEvent.press(firstDevice);
expect(mockOnPress).toHaveBeenCalledWith(
expect.objectContaining({
deviceType: expect.any(String),
}),
0,
);
});
});
Best Practices
Provider Placement
- App Level: Place provider at the app root for global access
- Feature Level: Use multiple providers for different device categories
- Screen Level: Scope providers to specific screens when needed
- Testing: Always wrap test components in provider
State Management
- Immutability: Always return new objects when updating state
- Batch Updates: Group related updates to minimize re-renders
- Validation: Validate state changes to prevent corruption
- Error Handling: Implement comprehensive error boundaries
Performance
- Memoization: Use useMemo for expensive computations
- Selectors: Create reusable selectors for common queries
- Debouncing: Debounce rapid state updates
- Lazy Loading: Load device data only when needed
Testing
- Unit Tests: Test hooks and components in isolation
- Integration Tests: Test provider with consuming components
- Error Cases: Test error boundaries and edge cases
- Performance: Monitor re-render frequency and optimization
API Reference
DeviceListProvider
interface DeviceListProviderProps {
children: React.ReactNode;
}
useDeviceList
interface UseDeviceListReturn {
deviceListItems: DeviceListItem[];
setDeviceListItems: React.Dispatch<React.SetStateAction<DeviceListItem[]>>;
}
DeviceListItem
interface DeviceListItem {
title: string; // i18n key for device title
subtitle: string; // i18n key for device subtitle
deviceImageUri: string; // Device image URI (base64 or URL)
status: DeviceStatus; // Current device status
deviceType: IntegratedDevices; // Device type identifier
}
Default Device List
The provider initializes with a default list of 15+ pre-configured medical devices including blood pressure monitors, glucose meters, thermometers, scales, pulse oximeters, and specialized devices.
Dependencies
react: Core React functionality@modules/bt-management: Device status and type definitions
Related Documentation
BTDeviceList: List component that consumes the contextBTDeviceItem: Individual device componentsBT Management: Core Bluetooth device management