Skip to main content

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

  1. App Level: Place provider at the app root for global access
  2. Feature Level: Use multiple providers for different device categories
  3. Screen Level: Scope providers to specific screens when needed
  4. Testing: Always wrap test components in provider

State Management

  1. Immutability: Always return new objects when updating state
  2. Batch Updates: Group related updates to minimize re-renders
  3. Validation: Validate state changes to prevent corruption
  4. Error Handling: Implement comprehensive error boundaries

Performance

  1. Memoization: Use useMemo for expensive computations
  2. Selectors: Create reusable selectors for common queries
  3. Debouncing: Debounce rapid state updates
  4. Lazy Loading: Load device data only when needed

Testing

  1. Unit Tests: Test hooks and components in isolation
  2. Integration Tests: Test provider with consuming components
  3. Error Cases: Test error boundaries and edge cases
  4. 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