Building an app with React-Native, Bluetooth Low Energy and Redux-Saga
Part 1 - Initial setup and scanning for devices
By Daniel Friyia, Agile Software Engineer, TribalScale

What are we building?
I thought I’d start this article series by first showing what we are trying to build and then explaining how to build it. Below is an app that will allow you to scan for a heart rate monitor, connect to that heart rate monitor and get the user’s heart rate. In this article, we are going to scan for devices, but by the end of Part 2, we will have an app that works like this:

Setting up the project
Now that we know what we’re going to build, let’s start generating the React-Native project using TypeScript. The instructions in this article can be used in Vanilla JS as well. Just remove the type annotations. To generate the project, run the following command in the terminal:
npx react-native init BLEReactNativeSample --template react-native-template-typescript
Setting up Redux
For this project, we will use Redux slices since they are a lot less code than your traditional Redux setup. I won’t go into detail here since this article is more about Redux-Saga and Bluetooth than Redux. We’ll make a simple setup using slices. To start, you’ll want to run the following command in the root of your project:
npm install redux react-redux redux-logger redux-saga @types/react-redux @reduxjs/toolkit @types/redux-logger
The Redux and React-Redux libraries are obviously for running Redux. We’ll use Redux-Saga for our async functionality with Bluetooth and redux-logger for debugging events and the store. The reducer is going to start in a bare-bones way. For now, we are just going to keep track of the devices available after a scan. You should make a redux directory and use it to hold all of these redux-related files. It should look something like this:
import {createSlice, PayloadAction} from '@reduxjs/toolkit';
type BluetoothState = {
availableDevices: Array<BluetoothPeripheral>;
};
const initialState: BluetoothState = {
availableDevices: [],
};
const bluetoothReducer = createSlice({
name: 'bluetooth',
initialState: initialState,
reducers: {
bluetoothPeripheralsFound: (
state: BluetoothState,
action: PayloadAction<Array<BluetoothPeripheral>>,
) => {
state.availableDevices = action.payload;
},
},
});
export const {
bluetoothPeripheralsFound
} = bluetoothReducer.actions
export default bluetoothReducer
Like all things TypeScript, we are going to make use of types to bring structure to the information we get back from the Bluetooth library. I created a models folder with the BluetoothPeripheral type. You should create this file as well under <project-root>/models/BluetoothPeripheral.ts.
export type BluetoothPeripheral = {
id: string;
name: string;
serviceUUIDs: Array<string>;
}
Next, we’ll configure the store. I won’t go into too much detail because any React developer has done this a million times. I am putting this here for copy/paste convenience:
import logger from 'redux-logger';
import {configureStore, combineReducers} from '@reduxjs/toolkit';
import bluetoothReducer from '../modules/Bluetooth/bluetooth.reducer';
import {useDispatch} from 'react-redux';
const rootReducer = combineReducers({
bluetooth: bluetoothReducer.reducer,
});
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) => {
return getDefaultMiddleware().concat(logger)
},
devTools: process.env.NODE_ENV === 'production'
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();
Finally, wrap your app component with a Provider like this and we can move on to setting up Bluetooth:
const App = () => {
return (
<Provider store={store}>
<Home />
</Provider>
);
};
const Home = () => {
const dispatch = useDispatch();
return (
<SafeAreaView style={styles.container}>
<Text>Hello world</Text>
<Button
title="Press Here"
onPress={() => {
dispatch(bluetoothPeripheralsFound(['AA:DD:CC:DD']));
}}
/>
</SafeAreaView>
);
};
At this point if you tap that button you should see AA:BB:CC:DD pop up in your redux logger. This should confirm that our slice setup is, in fact, working.
Setting up Bluetooth Low Energy in React Native
Here is where things get complicated and more interesting. We are going to set up Bluetooth to communicate with our React-Native app. In order to do this we’ll use the Bluetooth library react-native-ble-plx, which, at the time of writing, is the most popular in React-Native. First install the library into your project by running this command at the root of your project:
npm install --save react-native-ble-plx
Next we’ll need to follow some iOS and Android Specific instructions
iOS Specific Instructions
Because React-Native is written in Objective-C and react-native-ble-plx is written in Swift we’ll need a bridging header. This is relatively easy to create. First, create a new Swift file in XCode with any name. After that choose the option for a bridging header. It will look like this:

Next cd into the iOS folder for your React-Native project and type
npx pod install.
This should install the native dependencies needed for ios. After that add this to your ios/info.plist file under your key/value pairs.
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Our app uses bluetooth to find, connect and transfer data between different devices</string>
Finally, you’ll want to add Bluetoooth and background permissions so that Bluetooth can run while the app is in the background. You can do it on the Signing and Capabilities Screen:

Android Instructions
The first thing we’ll need to do is make sure our minSdk in the top level build.gradle is set to at least 18
buildscript {
ext {
...
minSdkVersion = 18
...
We’ll also want to make sure gitpack is added to our repositories
allprojects {
repositories {
...
maven { url 'https://www.jitpack.io' }
}
}
Finally in the Android Manifest add the following permissions:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"...<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>....
If you get lost modifying either of these files check out my build.gradle and AndroidManifest.xml in the sample project.
Testing initial setup
In order to test our setup we are going to do the most basic thing possible and just scan for Bluetooth devices. To do this we will add some quick throwaway code to our Home component. Remember to DELETE THIS AFTER TESTING 😱!
Add this to the top of your file under the imports:
const manager = new BleManager()
Then in the Home component add this method:
const scanForPeripherals = () => {
manager.startDeviceScan(null, null, (error, scannedDevice) => {
console.log(scannedDevice)
})
}
Finally make the onPress look like this:
onPress={() => {
dispatch(bluetoothPeripheralsFound(['AA:DD:CC:DD']));
scanForPeripherals()
}}
Now when you scan in the app you should see results in your debugger. Here is an example:

Wrapping Bluetooth
Next, we are going to wrap our Bluetooth library in a singleton object. There are a couple of reasons for this. First, we want every part of the app to share the same instance of BleManager. Second, if you have to swap out this library later on for another, it’s easier if you wrap your library. Let’s get started.
In the singleton, we will construct the BleManager and add our scan for peripherals code into it. This is primarily a copy/paste job. For those curious, null, null is passed into startDeviceScan because we want to scan for all devices available with no restrictions. The end result will end up looking like this:
import {BleError, BleManager, Device} from 'react-native-ble-plx';
class BluetoothLeManager {
bleManager: BleManager;
constructor() {
this.bleManager = new BleManager();
}
scanForPeripherals = (
onDeviceFound: (device: Device | null) => void,
onError: (error: BleError) => void,
) => {
this.bleManager.startDeviceScan(null, null, (error, scannedDevice) => {
if (error) {
return onError(error);
}
return onDeviceFound(scannedDevice);
});
};
stopScanningForPeripherals = () => {
this.bleManager.stopDeviceScan();
};
}
const bluetoothLeManager = new BluetoothLeManager();
export default bluetoothLeManager;
Configuring Redux-Saga
So we are all hooked up to Bluetooth, cool! Now we can move on to setting up Redux Saga to retrieve information from Bluetooth.
The first thing we’ll want to do is add Redux-Saga to our store file in the project. This is another one of those things that any React developer has done a million times, so I’ll just give a summary of the steps I used here.
First add a root saga, we’ll make the Bluetooth root saga in a minute
const sagaMiddleware = createSagaMiddleware();
const rootSaga = function* rootSaga() {
yield all([
fork(bluetoothSaga)
])
}
Then add the root saga to our middlewares:
middleware: getDefaultMiddleware => {
return getDefaultMiddleware().concat(logger).concat(sagaMiddleware);
},
Finally, start the middleware
sagaMiddleware.run(rootSaga)
Your store file should now look like this minus imports and typing
// .......
const sagaMiddleware = createSagaMiddleware();
const rootSaga = function* rootSaga() {
yield all([fork(bluetoothSaga)]);
};
const rootReducer = combineReducers({
bluetooth: bluetoothReducer.reducer,
});
export const store = configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware => {
return getDefaultMiddleware().concat(logger).concat(sagaMiddleware);
},
devTools: process.env.NODE_ENV === 'production',
});
sagaMiddleware.run(rootSaga);
// .......
Before creating our saga, we will need to adjust our reducer so we are able to handle device found events from the Bluetooth library. First, we will add this code which shows that we are scanning in the state. It’s here to initiate scanning for peripherals:
scanForPeripherals: state => {
state.isScanning = true;
},
Next, we’re going to export the action types so that we can use them in redux-saga
export const sagaActionConstants = {
SCAN_FOR_PERIPHERALS: bluetoothReducer.actions.scanForPeripherals.type,
ON_DEVICE_DISCOVERED: bluetoothReducer.actions.bluetoothPeripheralsFound.type,
};
Now let’s create a saga file for our Bluetooth actions. We’ll start by creating a root saga that listens for all actions. We will take the types from our bluetooth slice
export function* bluetoothSaga() {
yield takeEvery(
sagaActionConstants.SCAN_FOR_PERIPHERALS,
watchForPeripherals,
);
}
After this comes the more complex part. We’ll need to make a generator function that handles Bluetooth events as they come in from the library. This can be done in one method that I will break down here.
First, we’ll make a type to handle the device information that comes in:
type TakeableDevice = {
payload: {id: string; name: string; serviceUUIDs: string};
take: (cb: (message: any | END) => void) => Device;
};
Next, we need to create an Event Channel. Event Channels are used in Redux saga to pick up on spontaneous actions that can take place at any interval.
function* watchForPeripherals(): Generator<AnyAction, void, TakeableDevice> {
const onDiscoveredPeripheral = () => eventChannel(emitter => {
return bluetoothLeManager.scanForPeripherals(emitter);
});
After this, we’ll call our event channel as a function
const channel: TakeableChannel<unknown> = yield call(onDiscoveredPeripheral);
This next part looks a little weird because it is essentially an infinite loop that only ends when the application terminates. You may ask yourself, how does this not break the UI thread? The reason is because generator functions will pause until they get a result from yield. The JavaScript engine will then work on other things while it waits for a result to come back. This means that the infinite loop isn’t holding up the UI while waiting for events. This is the pattern recommended by Redux Saga and can be found here if you want more information.
try {
while (true) {
const response = yield take(channel);
Finally we’ll use put to send all information to the redux store
yield put({
type: sagaActionConstants.ON_DEVICE_DISCOVERED,
payload: {
id: response.payload.id,
name: response.payload.name,
serviceUUIDs: response.payload.serviceUUIDs,
},
});
}
} catch (e) {
console.log(e);
}
}
At this point we’ll want all these devices to be captured by Redux. We’ll modify onPeripheralsFound to weed out any duplicates and add our Bluetooth Peripherals to the state.
bluetoothPeripheralsFound: (
state: BluetoothState,
action: PayloadAction<BluetoothPeripheral>,
) => {
// Ensure no duplicate devices are added
const isDuplicate = state.availableDevices.some(
device => device.id === action.payload.id,
); if (!isDuplicate) {
state.availableDevices = state.availableDevices.concat(action.payload);
}
},
Taken together the code should look something like this:
type TakeableDevice = {
payload: {id: string; name: string; serviceUUIDs: string};
take: (cb: (message: any | END) => void) => Device;
};
function* watchForPeripherals(): Generator<AnyAction, void, TakeableDevice> {
const onDiscoveredPeripheral = () =>
eventChannel(emitter => {
return bluetoothLeManager.scanForPeripherals(emitter);
});
const channel: TakeableChannel<Device> = yield call(onDiscoveredPeripheral);
try {
while (true) {
const response = yield take(channel);
yield put({
type: sagaActionConstants.ON_DEVICE_DISCOVERED,
payload: {
id: response.payload.id,
name: response.payload.name,
serviceUUIDs: response.payload.serviceUUIDs,
},
});
}
} catch (e) {
console.log(e);
}
}
export function* bluetoothSaga() {
yield takeEvery(
sagaActionConstants.SCAN_FOR_PERIPHERALS,
watchForPeripherals,
);
}
Finally, we can finish up by displaying our peripherals to the screen to prove everything works. Simply get the peripheral information from the state and display it to the screen in string form. You can do it by adding this code to your Home component
const Home: FC = () => {
const dispatch = useDispatch();
const devices = useSelector(
(state: RootState) => state.bluetooth.availableDevices,
);
return (
<SafeAreaView style={styles.container}>
<ScrollView>
{devices.map(device => (
<>
<Text>{JSON.stringify(device)}</Text>
<View height={20} />
</>
))}
<Button
title="Press Here To Scan"
onPress={() => {
dispatch(scanForPeripherals());
}}
/>
</ScrollView>
</SafeAreaView>
);
};
And BOOM! We can get devices 🙂

This concludes part 1 of the series. Check out part 2 where we’ll connect to a device and retrieve data from it by using the infrastructure we created in this article.
Have questions about building an app with React-Native? Please mention in Comments.
Comments
Post a Comment
Thank You.