├── assets ├── logo_432.png └── logo_1024.png ├── src ├── constants │ └── Colors.js ├── navigator │ ├── Navigation.jsx │ └── StackNavigator.jsx ├── hooks │ ├── useAppStateReconnect.jsx │ └── useMqttConnection.jsx ├── Services │ ├── errorHandler.js │ └── mqttService.js ├── config │ └── environment.js ├── screen │ └── Mttq.jsx └── context │ └── MqttContext.jsx ├── metro.config.js ├── jsconfig.json ├── .env.example ├── babel.config.js ├── .gitignore ├── App.js ├── app.json ├── LICENSE ├── package.json └── README.md /assets/logo_432.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaximoLiberata/react-native-mqtt.js-example/HEAD/assets/logo_432.png -------------------------------------------------------------------------------- /assets/logo_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaximoLiberata/react-native-mqtt.js-example/HEAD/assets/logo_1024.png -------------------------------------------------------------------------------- /src/constants/Colors.js: -------------------------------------------------------------------------------- 1 | export const COLORS = { 2 | bg: '#000926', 3 | primary: '#001043', 4 | secondary: '#FEFEFE', 5 | text: '#FFFFFF', 6 | } 7 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require('expo/metro-config'); 2 | 3 | const config = getDefaultConfig(__dirname); 4 | 5 | module.exports = config; -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "src/*": ["./src/*"], 6 | "underscore": ["lodash"] 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/navigator/Navigation.jsx: -------------------------------------------------------------------------------- 1 | import { NavigationContainer } from '@react-navigation/native' 2 | import React from 'react' 3 | import Stacks from './StackNavigator' 4 | 5 | 6 | const Navigation = () => { 7 | return ( 8 | 9 | 10 | 11 | ) 12 | } 13 | 14 | 15 | export default Navigation 16 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # APP 2 | EXPO_PUBLIC_TZ = "UTC" 3 | EXPO_PUBLIC_EMIT_CONSOLE_LOGS = true 4 | 5 | # MQTT 6 | EXPO_PUBLIC_MQTT_HOST = "localhost" 7 | EXPO_PUBLIC_MQTT_PORT = 15675 8 | EXPO_PUBLIC_MQTT_PORT_SSL = 15676 9 | EXPO_PUBLIC_MQTT_SSL = false 10 | EXPO_PUBLIC_MQTT_VERSION = 5 11 | EXPO_PUBLIC_MQTT_QOS = 0 12 | EXPO_PUBLIC_MQTT_USERNAME = "consumer" 13 | EXPO_PUBLIC_MQTT_PASSWORD = "123" 14 | EXPO_PUBLIC_MQTT_TOPICS = "topic/1,topic/2,topicGeneric/#" 15 | 16 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true) 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: [ 6 | 'react-native-reanimated/plugin', 7 | [ 8 | 'module-resolver', 9 | { 10 | root: ['./src'], 11 | extensions: [ 12 | '.ios.js', 13 | '.android.js', 14 | '.js', 15 | '.jsx', 16 | '.ts', 17 | '.tsx', 18 | '.json', 19 | ], 20 | alias: { 21 | src: './src', 22 | }, 23 | }, 24 | ], 25 | ], 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # dependencie files 7 | pnpm-lock.yaml 8 | 9 | # Expo 10 | .expo/ 11 | dist/ 12 | web-build/ 13 | android/ 14 | 15 | # Native 16 | *.orig.* 17 | *.jks 18 | *.p8 19 | *.p12 20 | *.key 21 | *.mobileprovision 22 | 23 | # Metro 24 | .metro-health-check* 25 | 26 | # debug 27 | npm-debug.* 28 | yarn-debug.* 29 | yarn-error.* 30 | 31 | # macOS 32 | .DS_Store 33 | *.pem 34 | 35 | # local env files 36 | .env 37 | .env*.local 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import { StatusBar } from 'expo-status-bar' 2 | import { SafeAreaProvider } from 'react-native-safe-area-context' 3 | import { GestureHandlerRootView } from 'react-native-gesture-handler' 4 | import Navigation from './src/navigator/Navigation' 5 | import { MqttProvider } from 'src/context/MqttContext' 6 | 7 | 8 | export default function App() { 9 | 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/navigator/StackNavigator.jsx: -------------------------------------------------------------------------------- 1 | import { createNativeStackNavigator } from '@react-navigation/native-stack' 2 | import MttqScreen from 'src/screen/Mttq' 3 | import { COLORS } from 'src/constants/Colors' 4 | 5 | 6 | const Stack = createNativeStackNavigator() 7 | 8 | const stackOptions = { 9 | headerStyle: { 10 | backgroundColor: COLORS.bg, 11 | }, 12 | headerTransparent: true, 13 | headerTintColor: COLORS.text, 14 | headerTitleStyle: { 15 | fontWeight: 'bold', 16 | }, 17 | headerTitleAlign: 'center', 18 | } 19 | 20 | const Stacks = () => { 21 | return ( 22 | 23 | 28 | 29 | ) 30 | } 31 | 32 | 33 | export default Stacks 34 | -------------------------------------------------------------------------------- /src/hooks/useAppStateReconnect.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | import { AppState } from 'react-native' 3 | 4 | function useAppStateBackground(mqttClient) { 5 | 6 | const appState = useRef(AppState.currentState) 7 | 8 | useEffect(() => { 9 | 10 | const subscription = AppState.addEventListener('change', (nextAppState) => { 11 | 12 | if ( 13 | appState.current.match(/inactive|background/) && 14 | nextAppState === 'active' && 15 | mqttClient 16 | ) { 17 | mqttClient.reconnect() 18 | } else if (mqttClient) { 19 | mqttClient.end() 20 | } 21 | 22 | appState.current = nextAppState 23 | 24 | }) 25 | 26 | return () => { 27 | subscription.remove() 28 | } 29 | 30 | }, [mqttClient]) 31 | 32 | 33 | return appState.current 34 | 35 | } 36 | 37 | 38 | export default useAppStateBackground 39 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "mqtt-example", 4 | "slug": "mqtt-example", 5 | "platforms": [ 6 | "ios", 7 | "android", 8 | "web" 9 | ], 10 | "version": "3.0.0", 11 | "newArchEnabled": true, 12 | "orientation": "portrait", 13 | "scheme": "mqtt-example", 14 | "splash": { 15 | "image": "./assets/logo_1024.png", 16 | "resizeMode": "contain", 17 | "backgroundColor": "#ffffff" 18 | }, 19 | "updates": { 20 | "fallbackToCacheTimeout": 0 21 | }, 22 | "assetBundlePatterns": [ 23 | "**/*" 24 | ], 25 | "runtimeVersion": { 26 | "policy": "appVersion" 27 | }, 28 | "ios": { 29 | "supportsTablet": false, 30 | "bundleIdentifier": "com.example.mqttexample" 31 | }, 32 | "android": { 33 | "package": "com.example.mqttexample", 34 | "adaptiveIcon": { 35 | "foregroundImage": "./assets/logo_432.png", 36 | "backgroundColor": "#ffffff" 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Services/errorHandler.js: -------------------------------------------------------------------------------- 1 | import { envConfig } from 'src/config/environment'; 2 | 3 | 4 | /** 5 | * @typedef {"MqttTopic" | "MqttGeneral"} ErrorType 6 | */ 7 | 8 | /** 9 | * @param {Error} error 10 | * @returns {string} 11 | */ 12 | const processError = (error) => { 13 | return `Name: ${error?.name} || Message: ${error?.message} || Code: ${error?.code}`; 14 | } 15 | 16 | /** 17 | * @param {ErrorType} type 18 | * @param {Error} error 19 | */ 20 | const logError = (type, error) => { 21 | const errorMsg = processError(error); 22 | console.log(`${'[Error:' + type + ']'} || ${errorMsg}`); 23 | } 24 | 25 | /** 26 | * @param {(value: import('../hooks/useMqttConnection').MqttError) => void} callback 27 | * @param {ErrorType} type 28 | * @param {Error} error 29 | */ 30 | const emitStateError = (callback, type, error) => { 31 | 32 | const errorMsg = processError(error).replace(/ \|\|{0,2} /g, '\n'); 33 | 34 | callback({ 35 | type: '[Error:' + type + ']', 36 | msg: errorMsg 37 | }); 38 | 39 | if (envConfig.EMIT_CONSOLE_LOGS) { 40 | logError(type, error); 41 | } 42 | 43 | } 44 | 45 | 46 | export { 47 | logError, 48 | emitStateError 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/config/environment.js: -------------------------------------------------------------------------------- 1 | 2 | const envConfig = { 3 | TZ: process.env.EXPO_PUBLIC_TZ ?? 'UTC', 4 | MQTT_HOST: process.env.EXPO_PUBLIC_MQTT_HOST ?? 'localhost', 5 | MQTT_SSL: process.env.EXPO_PUBLIC_MQTT_SSL === 'true', 6 | MQTT_PORT: typeof process.env.EXPO_PUBLIC_MQTT_PORT === 'string' ? parseInt(process.env.EXPO_PUBLIC_MQTT_PORT) : 0, 7 | MQTT_PORT_SSL: typeof process.env.EXPO_PUBLIC_MQTT_PORT_SSL === 'string' ? parseInt(process.env.EXPO_PUBLIC_MQTT_PORT_SSL) : 0, 8 | MQTT_VERSION: typeof process.env.EXPO_PUBLIC_MQTT_VERSION === 'string' ? parseInt(process.env.EXPO_PUBLIC_MQTT_VERSION) : 0, 9 | MQTT_QOS: typeof process.env.EXPO_PUBLIC_MQTT_QOS === 'string' ? parseInt(process.env.EXPO_PUBLIC_MQTT_QOS) : 0, 10 | MQTT_USERNAME: (process.env.EXPO_PUBLIC_MQTT_USERNAME ?? '').length === 0 ? undefined : process.env.EXPO_PUBLIC_MQTT_USERNAME, 11 | MQTT_PASSWORD: (process.env.EXPO_PUBLIC_MQTT_PASSWORD ?? '').length === 0 ? undefined : process.env.EXPO_PUBLIC_MQTT_PASSWORD, 12 | MQTT_TOPICS: typeof process.env.EXPO_PUBLIC_MQTT_TOPICS === 'string' ? process.env.EXPO_PUBLIC_MQTT_TOPICS.split(',') : [], 13 | EMIT_CONSOLE_LOGS: process.env.EXPO_PUBLIC_EMIT_CONSOLE_LOGS === 'true', 14 | } 15 | 16 | 17 | export { 18 | envConfig 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mqtt-app-example", 3 | "version": "3.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web", 10 | "tunel": "npx expo start -c --tunnel", 11 | "android-build": "eas build -p android --profile preview", 12 | "ios-build": "eas build -p ios --profile preview" 13 | }, 14 | "engines": { 15 | "node": ">=20.19.4" 16 | }, 17 | "dependencies": { 18 | "@expo/metro-runtime": "~6.1.2", 19 | "@react-navigation/native": "7.1.22", 20 | "@react-navigation/native-stack": "7.8.1", 21 | "expo": "~54.0.25", 22 | "expo-crypto": "~15.0.7", 23 | "expo-status-bar": "~3.0.8", 24 | "expo-updates": "~29.0.13", 25 | "mqtt": "5.14.1", 26 | "react": "19.1.0", 27 | "react-dom": "19.1.0", 28 | "react-native": "0.81.5", 29 | "react-native-gesture-handler": "~2.28.0", 30 | "react-native-reanimated": "~4.1.5", 31 | "react-native-safe-area-context": "~5.6.0", 32 | "react-native-screens": "~4.16.0", 33 | "react-native-web": "~0.21.2", 34 | "react-native-worklets": "0.5.1" 35 | }, 36 | "devDependencies": { 37 | "@babel/core": "7.28.5", 38 | "babel-plugin-module-resolver": "5.0.2" 39 | }, 40 | "private": true 41 | } 42 | -------------------------------------------------------------------------------- /src/hooks/useMqttConnection.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { createMqttClient } from 'src/Services/mqttService' 3 | 4 | 5 | /** 6 | * @typedef {'Connected' | 'Disconnected' | 'Offline' | 'Reconnecting' | 'Error'} MqttStatus 7 | */ 8 | 9 | /** 10 | * @typedef {{ type: string, msg: string }} MqttError 11 | */ 12 | 13 | /** 14 | * @typedef {{ message: any, topic: string }} MqttData 15 | */ 16 | 17 | /** 18 | * @typedef {import('mqtt').MqttClient} MqttClient 19 | */ 20 | 21 | function useMqttConnection(doMqttConnection) { 22 | 23 | /** 24 | * @type [MqttStatus, React.Dispatch] 25 | */ 26 | const [mqttStatus, setMqttStatus] = useState('Disconnected') 27 | 28 | /** 29 | * @type [MqttError, React.Dispatch] 30 | */ 31 | const [mqttError, setMqttError] = useState({}) 32 | 33 | /** 34 | * @type [MqttData, React.Dispatch] 35 | */ 36 | const [mqttData, setMqttData] = useState({}) 37 | 38 | /** 39 | * @type [MqttClient, React.Dispatch] 40 | */ 41 | const [mqttClient, setMqttClient] = useState(null) 42 | 43 | useEffect(() => { 44 | 45 | if (!doMqttConnection) return 46 | 47 | const client = createMqttClient({ 48 | setMqttStatus, 49 | setMqttError, 50 | uniqueId: 'react-native-0000', 51 | onMessage: (topic, message) => { 52 | setMqttData(() => ({ 53 | message, 54 | topic 55 | })) 56 | }, 57 | }) 58 | 59 | setMqttClient(client) 60 | 61 | 62 | return () => { 63 | if (client) { 64 | client.end() 65 | } 66 | } 67 | 68 | }, [doMqttConnection]) 69 | 70 | 71 | return { 72 | mqttClient, 73 | mqttData, 74 | mqttStatus, 75 | mqttError, 76 | setMqttStatus, 77 | setMqttError 78 | } 79 | 80 | } 81 | 82 | export default useMqttConnection 83 | -------------------------------------------------------------------------------- /src/screen/Mttq.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { Button, StyleSheet, Text, View } from 'react-native' 3 | import { envConfig } from 'src/config/environment' 4 | import { useMqtt } from 'src/context/MqttContext' 5 | 6 | 7 | let dateLastTopicRecived = 'N/A'; 8 | 9 | const MttqScreen = () => { 10 | 11 | const { mqttClient, mqttData, mqttStatus, mqttError, subscribeToTopic } = useMqtt() 12 | 13 | useEffect(() => { 14 | subscribeToTopic(envConfig.MQTT_TOPICS, { qos: envConfig.MQTT_QOS }) 15 | }, []) 16 | 17 | /** 18 | * @param {string | null | undefined} topic 19 | */ 20 | const lastTopicRecived = (topic) => { 21 | if (topic) { 22 | dateLastTopicRecived = (new Date()).toLocaleString('af-ZA', { timeZone: envConfig.TZ }) 23 | } 24 | 25 | return dateLastTopicRecived; 26 | } 27 | 28 | return ( 29 | 37 | [Connection Status] 38 | {mqttStatus} 39 | 40 | [Topic] 41 | {mqttData?.topic ?? 'N/A'} 42 | 43 | [Date Last Topic Received] 44 | {lastTopicRecived(mqttData?.topic)} 45 | 46 | 47 | {mqttError?.type ?? '[Error:N/A]'} 48 | 49 | 50 | {mqttError?.msg ?? ''} 51 | 52 | 53 | 59 |