├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── prepare-commit-msg ├── .lintstagedrc.json ├── .npmignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE.MD ├── README.md ├── SECURITY.md ├── __tests__ ├── Connector.spec.tsx ├── connection.ts └── useSubscription.spec.tsx ├── commitlint.config.js ├── declarations.d.ts ├── jest.config.js ├── lib ├── Connector.tsx ├── Context.tsx ├── index.ts ├── setupTest.tsx ├── types.ts ├── useMqttState.tsx └── useSubscription.tsx ├── package.json ├── rollup.config.js ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | jest: true, 6 | }, 7 | globals: { 8 | Atomics: 'readonly', 9 | SharedArrayBuffer: 'readonly', 10 | }, 11 | parserOptions: { 12 | ecmaVersion: 2018, 13 | ecmaFeatures: { 14 | jsx: true, 15 | tsx: true, 16 | }, 17 | sourceType: 'module', 18 | }, 19 | parser: '@typescript-eslint/parser', 20 | plugins: [ 21 | '@typescript-eslint', 22 | 'import', 23 | 'import-helpers', 24 | 'react-hooks', 25 | 'jest', 26 | 'prettier', 27 | ], 28 | extends: [ 29 | 'airbnb', 30 | 'plugin:@typescript-eslint/recommended', 31 | 'prettier', 32 | 'plugin:prettier/recommended', 33 | ], 34 | rules: { 35 | 'prettier/prettier': 'error', 36 | 'class-methods-use-this': 'off', 37 | '@typescript-eslint/no-explicit-any': 'off', 38 | '@typescript-eslint/interface-name-prefix': 'off', 39 | '@typescript-eslint/no-var-requires': 'off', 40 | '@typescript-eslint/explicit-function-return-type': 'off', 41 | 'react/jsx-filename-extension': [ 42 | 'warn', 43 | { 44 | extensions: ['.jsx', '.tsx'], 45 | }, 46 | ], 47 | 'react/jsx-filename-extension': [ 48 | 'warn', 49 | { 50 | extensions: ['.jsx', '.tsx'], 51 | }, 52 | ], 53 | 'import-helpers/order-imports': [ 54 | 'warn', 55 | { 56 | newlinesBetween: 'always', 57 | groups: ['/^react/', 'module', '/^~/', ['parent', 'sibling', 'index']], 58 | alphabetize: { order: 'asc', ignoreCase: true }, 59 | }, 60 | ], 61 | 'import/no-dynamic-require': 'off', 62 | 'no-param-reassign': 'off', 63 | 'no-unused-expressions': 'off', 64 | 'no-underscore-dangle': 'off', 65 | 'react/prop-types': 'off', 66 | 'jsx-a11y/label-has-for': 'off', 67 | 'import/prefer-default-export': 'off', 68 | 'react-hooks/rules-of-hooks': 'error', 69 | 'react-hooks/exhaustive-deps': 'warn', 70 | 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], 71 | 'import/extensions': [ 72 | 'error', 73 | 'ignorePackages', 74 | { 75 | ts: 'never', 76 | tsx: 'never', 77 | }, 78 | ], 79 | }, 80 | settings: { 81 | 'import/extensions': ['.js', '.jsx', '.ts', '.tsx'], 82 | 'import/parsers': { 83 | '@typescript-eslint/parser': ['.ts', '.tsx'], 84 | }, 85 | 'import/resolver': { 86 | typescript: {}, 87 | }, 88 | }, 89 | }; 90 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Quality and Build 5 | on: 6 | push: 7 | branches: [master, develop] 8 | pull_request: 9 | branches: '*' 10 | 11 | jobs: 12 | quality: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [14.x, 16.x] 18 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v2 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | cache: 'yarn' 27 | - run: yarn 28 | - run: yarn test 29 | 30 | publish: 31 | runs-on: ubuntu-latest 32 | if: ${{github.ref == 'refs/heads/master' }} 33 | needs: [quality] 34 | steps: 35 | - uses: actions/checkout@v2 36 | - name: Use Node.js ${{ matrix.node-version }} 37 | uses: actions/setup-node@v2 38 | with: 39 | node-version: ${{ matrix.node-version }} 40 | cache: 'yarn' 41 | - run: yarn 42 | - run: yarn build 43 | - run: yarn semantic-release 44 | env: 45 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage/ 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variable files 55 | .env* 56 | 57 | # Library 58 | dist/ 59 | typings/ 60 | 61 | # gatsby files 62 | .cache/ 63 | public/ 64 | 65 | # Mac files 66 | .DS_Store 67 | 68 | # Yarn 69 | yarn-error.log 70 | .pnp/ 71 | .pnp.js 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | exec < /dev/tty && npx cz --hook || true 5 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "lib/*.{js,ts,tsx}": ["eslint --ext js,ts,tsx --fix"] 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | dist 4 | example 5 | __tests__ 6 | .github 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "arrowParens": "avoid" 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.0.1](https://github.com/victorHAS/mqtt-react-hooks/compare/v2.0.0...v2.0.1) (2021-09-05) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * wrong validation in useSubscription ([859fbe5](https://github.com/victorHAS/mqtt-react-hooks/commit/859fbe5f316b8500abb0d59aa84114376ec18978)) 7 | -------------------------------------------------------------------------------- /LICENSE.MD: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Victor Hermes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![npm](https://img.shields.io/npm/v/mqtt-react-hooks?color=blue)](https://www.npmjs.com/package/mqtt-react-hooks) 4 | [![Quality and Build](https://github.com/VictorHAS/mqtt-react-hooks/actions/workflows/publish.yml/badge.svg)](https://github.com/VictorHAS/mqtt-react-hooks/actions/workflows/publish.yml) 5 | 6 |
7 | 8 | ## Overview 9 | 10 | This library is focused in help you to connect, publish and subscribe to a Message Queuing Telemetry Transport (MQTT) in ReactJS with the power of React Hooks. 11 | 12 | ## Flow of Data 13 | 14 | 1. WiFi or other mobile sensors publish data to an MQTT broker 15 | 2. ReactJS subscribes to the MQTT broker and receives the data using MQTT.js 16 | 3. React's state is updated and the data is passed down to stateless components 17 | 18 | ## Key features 19 | 20 | - React Hooks; 21 | - Beautiful syntax; 22 | - Performance focused; 23 | 24 | ## Installation 25 | 26 | Just add mqtt-react-hooks to your project: 27 | 28 | ``` 29 | yarn add mqtt-react-hooks 30 | ``` 31 | 32 | ### Hooks availables 33 | 34 | - useMqttState -> return { connectionStatus, client, message } 35 | - useSubscription(topic: string | string[], options?: {} ) -> return { client, topic, message, connectionStatus } 36 | 37 | ### Usage 38 | 39 | Currently, mqtt-react-hooks exports one enhancers. 40 | Similarly to react-redux, you'll have to first wrap a root component with a 41 | `Connector` which will initialize the mqtt instance. 42 | 43 | #### Root component 44 | 45 | The only property for the connector is the connection information for [mqtt.Client#connect](https://github.com/mqttjs/MQTT.js#connect) 46 | 47 | **Example Root component:** 48 | 49 | ```js 50 | import React from 'react'; 51 | 52 | import { Connector } from 'mqtt-react-hooks'; 53 | import Status from './Status'; 54 | 55 | export default function App() { 56 | return ( 57 | 58 | 59 | 60 | ); 61 | } 62 | ``` 63 | 64 | **Example Connection Status** 65 | 66 | ```js 67 | import React from 'react'; 68 | 69 | import { useMqttState } from 'mqtt-react-hooks'; 70 | 71 | export default function Status() { 72 | /* 73 | * Status list 74 | * - Offline 75 | * - Connected 76 | * - Reconnecting 77 | * - Closed 78 | * - Error: printed in console too 79 | */ 80 | const { connectionStatus } = useMqttState(); 81 | 82 | return

{`Status: ${connectionStatus}`}

; 83 | } 84 | ``` 85 | 86 | #### Subscribe 87 | 88 | **Example Posting Messages** 89 | 90 | MQTT Client is passed on useMqttState and can be used to publish messages via 91 | [mqtt.Client#publish](https://github.com/mqttjs/MQTT.js#publish) and don't need Subscribe 92 | 93 | ```js 94 | import React from 'react'; 95 | import { useMqttState } from 'mqtt-react-hooks'; 96 | 97 | export default function Status() { 98 | const { client } = useMqttState(); 99 | 100 | function handleClick(message) { 101 | return client.publish('esp32/led', message); 102 | } 103 | 104 | return ( 105 | 108 | ); 109 | } 110 | ``` 111 | 112 | **Example Subscribing and Receiving messages** 113 | 114 | ```js 115 | import React from 'react'; 116 | 117 | import { useSubscription } from 'mqtt-react-hooks'; 118 | 119 | export default function Status() { 120 | /* Message structure: 121 | * topic: string 122 | * message: string 123 | */ 124 | const { message } = useSubscription([ 125 | 'room/esp32/testing', 126 | 'room/esp32/light', 127 | ]); 128 | 129 | return ( 130 | <> 131 |
132 | {`topic:${message.topic} - message: ${message.message}`} 133 |
134 | 135 | ); 136 | } 137 | ``` 138 | 139 | ## Tips 140 | 141 | 1. If you need to change the format in which messages will be inserted in message useState, you can pass the option of parserMethod in the Connector: 142 | 143 | ```js 144 | import React, { useEffect, useState } from 'react'; 145 | import { Connector, useSubscription } from 'mqtt-react-hooks'; 146 | 147 | const Children = () => { 148 | const { message, connectionStatus } = useSubscription('esp32/testing/#'); 149 | const [messages, setMessages] = useState < any > []; 150 | 151 | useEffect(() => { 152 | if (message) setMessages((msgs: any) => [...msgs, message]); 153 | }, [message]); 154 | 155 | return ( 156 | <> 157 | {connectionStatus} 158 |
159 | {JSON.stringify(messages)} 160 | 161 | ); 162 | }; 163 | 164 | const App = () => { 165 | return ( 166 | msg} // msg is Buffer 169 | > 170 | 171 | 172 | ); 173 | }; 174 | ``` 175 | 176 | ## Contributing 177 | 178 | Thanks for being interested on making this package better. We encourage everyone to help improving this project with some new features, bug fixes and performance issues. Please take a little bit of your time to read our guides, so this process can be faster and easier. 179 | 180 | ## License 181 | 182 | MIT © 183 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /__tests__/Connector.spec.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import React from 'react'; 6 | 7 | import { renderHook, act, waitFor } from '@testing-library/react'; 8 | 9 | import { useMqttState, Connector } from '../lib'; 10 | import { URL, options } from './connection'; 11 | 12 | let wrapper; 13 | 14 | describe('Connector wrapper', () => { 15 | beforeAll(() => { 16 | wrapper = ({ children }) => ( 17 | 18 | {children} 19 | 20 | ); 21 | }); 22 | 23 | it('should not connect with mqtt, wrong url', async () => { 24 | const { result } = renderHook(() => useMqttState(), { 25 | wrapper: ({ children }) => ( 26 | 30 | {children} 31 | 32 | ), 33 | }); 34 | 35 | await waitFor(() => expect(result.current.connectionStatus).toBe('Offline')); 36 | }); 37 | 38 | it('should connect with mqtt', async () => { 39 | const { result } = renderHook(() => useMqttState(), { 40 | wrapper, 41 | }); 42 | 43 | await waitFor(() => expect(result.current.client?.connected).toBe(true)); 44 | 45 | expect(result.current.connectionStatus).toBe('Connected'); 46 | 47 | await act(async () => { 48 | result.current.client?.end(); 49 | }); 50 | }); 51 | 52 | it('should connect passing props', async () => { 53 | const { result } = renderHook(() => useMqttState(), { 54 | wrapper: ({ children }) => ( 55 | 59 | {children} 60 | 61 | ), 62 | }); 63 | 64 | await waitFor(() => expect(result.current.client?.connected).toBe(true)); 65 | 66 | expect(result.current.connectionStatus).toBe('Connected'); 67 | 68 | await act(async () => { 69 | result.current.client?.end(); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /__tests__/connection.ts: -------------------------------------------------------------------------------- 1 | export const URL = 'mqtt://test.mosquitto.org:1883'; 2 | export const options = { 3 | clientId: `testing-mqtt-react-hooks`, 4 | }; 5 | -------------------------------------------------------------------------------- /__tests__/useSubscription.spec.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import React from 'react'; 6 | 7 | import { cleanup, renderHook, waitFor } from '@testing-library/react'; 8 | 9 | import { Connector, useSubscription } from '../lib'; 10 | import { URL, options } from './connection'; 11 | 12 | const TOPIC = 'mqtt/react/hooks/test'; 13 | 14 | let wrapper; 15 | 16 | describe('useSubscription', () => { 17 | beforeAll(() => { 18 | wrapper = ({ children }) => ( 19 | 20 | {children} 21 | 22 | ); 23 | }); 24 | 25 | afterEach(cleanup) 26 | 27 | it('should get message on topic test', async () => { 28 | const { result } = renderHook( 29 | () => useSubscription(TOPIC), 30 | { 31 | wrapper, 32 | }, 33 | ); 34 | 35 | await waitFor(() => expect(result.current.client?.connected).toBe(true)); 36 | 37 | const message = 'testing message'; 38 | result.current.client?.publish(TOPIC, message, (err) => { 39 | expect(err).toBeFalsy() 40 | }); 41 | 42 | await waitFor(() => expect(result.current?.message?.message).toBe(message)); 43 | }); 44 | 45 | it('should get message on topic with single selection of the path + ', async () => { 46 | const { result } = renderHook( 47 | () => useSubscription(`${TOPIC}/+/test/+/selection`), 48 | { 49 | wrapper, 50 | }, 51 | ); 52 | 53 | await waitFor(() => expect(result.current.client?.connected).toBe(true)); 54 | 55 | const message = 'testing single selection message'; 56 | 57 | result.current.client?.publish( 58 | `${TOPIC}/match/test/single/selection`, 59 | message, 60 | ); 61 | 62 | await waitFor(() => expect(result.current.message?.message).toBe(message)); 63 | }); 64 | 65 | it('should get message on topic with # wildcard', async () => { 66 | const { result } = renderHook( 67 | () => useSubscription(`${TOPIC}/#`), 68 | { 69 | wrapper, 70 | }, 71 | ); 72 | 73 | await waitFor(() => expect(result.current.client?.connected).toBe(true)); 74 | 75 | const message = 'testing with # wildcard'; 76 | 77 | result.current.client?.publish(`${TOPIC}/match/test/wildcard`, message); 78 | 79 | await waitFor(() => expect(result.current.message?.message).toBe(message)); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'mqtt-pattern' { 2 | const value: any; 3 | export = value; 4 | } 5 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | 3 | module.exports = { 4 | bail: true, 5 | collectCoverage: true, 6 | collectCoverageFrom: ['lib/*.tsx', '!lib/setupTest.tsx'], 7 | coverageDirectory: '__tests__/coverage', 8 | coverageReporters: ['json', 'lcov'], 9 | coverageThreshold: { 10 | global: { 11 | branches: 70, 12 | functions: 80, 13 | lines: 80, 14 | statements: 80, 15 | }, 16 | }, 17 | clearMocks: true, 18 | testMatch: [join(__dirname, '__tests__/*.spec.{ts,tsx}')], 19 | transform: { 20 | '^.+\\.(ts|tsx)$': 'ts-jest', 21 | }, 22 | moduleFileExtensions: ['ts', 'tsx', 'js'], 23 | }; 24 | -------------------------------------------------------------------------------- /lib/Connector.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useMemo, useRef } from 'react'; 2 | 3 | import { connect, MqttClient } from 'mqtt'; 4 | 5 | import MqttContext from './Context'; 6 | import { Error, ConnectorProps, IMqttContext } from './types'; 7 | 8 | export default function Connector({ 9 | children, 10 | brokerUrl, 11 | options = { keepalive: 0 }, 12 | parserMethod, 13 | }: ConnectorProps) { 14 | // Using a ref rather than relying on state because it is synchronous 15 | const clientValid = useRef(false); 16 | const [connectionStatus, setStatus] = useState('Offline'); 17 | const [client, setClient] = useState(null); 18 | 19 | useEffect(() => { 20 | if (!client && !clientValid.current) { 21 | // This synchronously ensures we won't enter this block again 22 | // before the client is asynchronously set 23 | clientValid.current = true; 24 | setStatus('Connecting'); 25 | console.log(`attempting to connect to ${brokerUrl}`); 26 | const mqtt = connect(brokerUrl, options); 27 | mqtt.on('connect', () => { 28 | console.debug('on connect'); 29 | setStatus('Connected'); 30 | // For some reason setting the client as soon as we get it from connect breaks things 31 | setClient(mqtt); 32 | }); 33 | mqtt.on('reconnect', () => { 34 | console.debug('on reconnect'); 35 | setStatus('Reconnecting'); 36 | }); 37 | mqtt.on('error', err => { 38 | console.log(`Connection error: ${err}`); 39 | setStatus(err.message); 40 | }); 41 | mqtt.on('offline', () => { 42 | console.debug('on offline'); 43 | setStatus('Offline'); 44 | }); 45 | mqtt.on('end', () => { 46 | console.debug('on end'); 47 | setStatus('Offline'); 48 | }); 49 | } 50 | }, [client, clientValid, brokerUrl, options]); 51 | 52 | // Only do this when the component unmounts 53 | useEffect( 54 | () => () => { 55 | if (client) { 56 | console.log('closing mqtt client'); 57 | client.end(true); 58 | setClient(null); 59 | clientValid.current = false; 60 | } 61 | }, 62 | [client, clientValid], 63 | ); 64 | 65 | // This is to satisfy 66 | // https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-constructed-context-values.md 67 | const value: IMqttContext = useMemo( 68 | () => ({ 69 | connectionStatus, 70 | client, 71 | parserMethod, 72 | }), 73 | [connectionStatus, client, parserMethod], 74 | ); 75 | 76 | return {children}; 77 | } 78 | -------------------------------------------------------------------------------- /lib/Context.tsx: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | 3 | import { createContext } from 'react'; 4 | 5 | import { IMqttContext } from './types'; 6 | 7 | export default createContext({} as IMqttContext); 8 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useSubscription } from './useSubscription'; 2 | export { default as useMqttState } from './useMqttState'; 3 | 4 | export { default as Connector } from './Connector'; 5 | 6 | export * from './types'; 7 | 8 | export { default as MqttContext } from './Context'; 9 | -------------------------------------------------------------------------------- /lib/setupTest.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/react'; 2 | import '@testing-library/jest-dom'; 3 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | import { MqttClient, IClientOptions } from 'mqtt'; 2 | 3 | export interface Error { 4 | name: string; 5 | message: string; 6 | stack?: string; 7 | } 8 | 9 | export interface ConnectorProps { 10 | brokerUrl: string; 11 | options?: IClientOptions; 12 | parserMethod?: (message) => string; 13 | children: React.ReactNode; 14 | } 15 | 16 | export interface IMqttContext { 17 | connectionStatus: string | Error; 18 | client?: MqttClient | null; 19 | parserMethod?: (message: any) => string; 20 | } 21 | 22 | export interface IMessageStructure { 23 | [key: string]: string; 24 | } 25 | 26 | export interface IMessage { 27 | topic: string; 28 | message?: string | IMessageStructure; 29 | } 30 | 31 | export interface IUseSubscription { 32 | topic: string | string[]; 33 | client?: MqttClient | null; 34 | message?: IMessage; 35 | connectionStatus: string | Error; 36 | } 37 | 38 | export type Omit = Pick>; 39 | -------------------------------------------------------------------------------- /lib/useMqttState.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | 3 | import MqttContext from './Context'; 4 | import { IMqttContext as Context } from './types'; 5 | 6 | export default function useMqttState() { 7 | const { connectionStatus, client, parserMethod } = useContext( 8 | MqttContext, 9 | ); 10 | 11 | return { 12 | connectionStatus, 13 | client, 14 | parserMethod, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /lib/useSubscription.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useCallback, useState } from 'react'; 2 | 3 | import { IClientSubscribeOptions } from 'mqtt'; 4 | import { matches } from 'mqtt-pattern'; 5 | 6 | import MqttContext from './Context'; 7 | import { IMqttContext as Context, IUseSubscription, IMessage } from './types'; 8 | 9 | export default function useSubscription( 10 | topic: string | string[], 11 | options: IClientSubscribeOptions = {} as IClientSubscribeOptions, 12 | ): IUseSubscription { 13 | const { client, connectionStatus, parserMethod } = useContext( 14 | MqttContext, 15 | ); 16 | 17 | const [message, setMessage] = useState(undefined); 18 | 19 | const subscribe = useCallback(async () => { 20 | client?.subscribe(topic, options); 21 | }, [client, options, topic]); 22 | 23 | const callback = useCallback( 24 | (receivedTopic: string, receivedMessage: any) => { 25 | if ([topic].flat().some(rTopic => matches(rTopic, receivedTopic))) { 26 | setMessage({ 27 | topic: receivedTopic, 28 | message: 29 | parserMethod?.(receivedMessage) || receivedMessage.toString(), 30 | }); 31 | } 32 | }, 33 | [parserMethod, topic], 34 | ); 35 | 36 | useEffect(() => { 37 | if (client?.connected) { 38 | subscribe(); 39 | 40 | client.on('message', callback); 41 | } 42 | return () => { 43 | client?.off('message', callback); 44 | }; 45 | }, [callback, client, subscribe]); 46 | 47 | return { 48 | client, 49 | topic, 50 | message, 51 | connectionStatus, 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mqtt-react-hooks", 3 | "version": "3.0.0-alpha.1", 4 | "private": false, 5 | "description": "ReactJS library for Pub/Sub communication with an MQTT broker using Hooks", 6 | "main": "dist/index.js", 7 | "module": "dist/index.es.js", 8 | "types": "dist/index.d.ts", 9 | "directories": { 10 | "lib": "lib" 11 | }, 12 | "files": [ 13 | "dist" 14 | ], 15 | "publishConfig": { 16 | "access": "public" 17 | }, 18 | "keywords": [ 19 | "mqtt", 20 | "react", 21 | "hooks", 22 | "context", 23 | "esp8266", 24 | "esp32" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/victorHAS/mqtt-react-hooks.git" 29 | }, 30 | "homepage": "https://github.com/victorHAS/mqtt-react-hooks#readme", 31 | "author": "VictorHAS", 32 | "license": "MIT", 33 | "release": { 34 | "plugins": [ 35 | "@semantic-release/commit-analyzer", 36 | "@semantic-release/release-notes-generator", 37 | [ 38 | "@semantic-release/npm", 39 | { 40 | "npmPublish": false 41 | } 42 | ], 43 | "@semantic-release/changelog", 44 | "@semantic-release/git" 45 | ], 46 | "branch": "master" 47 | }, 48 | "scripts": { 49 | "prebuild": "rimraf dist typings", 50 | "build": "rollup -c ./rollup.config.js", 51 | "test": "jest --runInBand --detectOpenHandles --silent", 52 | "coveralls": "jest && cat ./__tests__/coverage/lcov.info | coveralls", 53 | "prepare": "husky install" 54 | }, 55 | "devDependencies": { 56 | "@babel/core": "^7.17.10", 57 | "@commitlint/cli": "^16.2.4", 58 | "@commitlint/config-conventional": "^16.2.4", 59 | "@rollup/plugin-commonjs": "^22.0.0", 60 | "@rollup/plugin-json": "^4.1.0", 61 | "@rollup/plugin-node-resolve": "^13.3.0", 62 | "@rollup/plugin-url": "^7.0.0", 63 | "@semantic-release/changelog": "^6.0.1", 64 | "@semantic-release/commit-analyzer": "^9.0.2", 65 | "@semantic-release/git": "^10.0.1", 66 | "@semantic-release/npm": "^9.0.1", 67 | "@semantic-release/release-notes-generator": "^10.0.3", 68 | "@testing-library/dom": "8.13.0", 69 | "@testing-library/jest-dom": "^5.16.4", 70 | "@testing-library/react": "13.2.0", 71 | "@types/jest": "^27.5.0", 72 | "@types/node": "^17.0.32", 73 | "@types/react": "^18.0.9", 74 | "@types/react-dom": "^18.0.3", 75 | "@types/react-router-dom": "^5.3.3", 76 | "@types/react-test-renderer": "^18.0.0", 77 | "@typescript-eslint/eslint-plugin": "^5.23.0", 78 | "@typescript-eslint/parser": "^5.23.0", 79 | "commitizen": "^4.2.4", 80 | "coveralls": "^3.1.1", 81 | "cz-conventional-changelog": "3.3.0", 82 | "eslint": "^8.15.0", 83 | "eslint-config-airbnb": "^19.0.4", 84 | "eslint-config-prettier": "^8.5.0", 85 | "eslint-import-resolver-typescript": "^2.7.1", 86 | "eslint-plugin-import": "^2.26.0", 87 | "eslint-plugin-import-helpers": "^1.2.1", 88 | "eslint-plugin-jest": "^26.1.5", 89 | "eslint-plugin-jsx-a11y": "^6.5.1", 90 | "eslint-plugin-prettier": "^4.0.0", 91 | "eslint-plugin-react": "^7.29.4", 92 | "eslint-plugin-react-hooks": "^4.5.0", 93 | "husky": "^8.0.0", 94 | "jest": "^28.1.0", 95 | "jest-environment-jsdom": "^28.1.0", 96 | "lint-staged": "^12.4.1", 97 | "npm-run-all": "^4.1.5", 98 | "prettier": "^2.6.2", 99 | "react": "^18.1.0", 100 | "react-dom": "18.1.0", 101 | "react-router-dom": "^6.3.0", 102 | "react-test-renderer": "18.1.0", 103 | "rimraf": "^3.0.2", 104 | "rollup": "^2.72.1", 105 | "rollup-plugin-babel": "^4.3.2", 106 | "rollup-plugin-peer-deps-external": "^2.2.4", 107 | "rollup-plugin-terser": "^7.0.2", 108 | "rollup-plugin-typescript2": "^0.31.2", 109 | "semantic-release": "^19.0.2", 110 | "ts-jest": "^28.0.2", 111 | "typescript": "^4.6.4" 112 | }, 113 | "peerDependencies": { 114 | "react": ">= 18.1.0", 115 | "react-dom": ">= 18.1.0" 116 | }, 117 | "dependencies": { 118 | "mqtt": "4.3.7", 119 | "mqtt-pattern": "^1.2.0" 120 | }, 121 | "config": { 122 | "commitizen": { 123 | "path": "./node_modules/cz-conventional-changelog" 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import url from '@rollup/plugin-url'; 4 | import path from 'path'; 5 | import babel from 'rollup-plugin-babel'; 6 | import { terser } from 'rollup-plugin-terser'; 7 | import typescript from 'rollup-plugin-typescript2'; 8 | 9 | const PACKAGE_ROOT_PATH = process.cwd(); 10 | const INPUT_FILE = path.join(PACKAGE_ROOT_PATH, 'lib/index.ts'); 11 | const pkg = require(path.join(PACKAGE_ROOT_PATH, 'package.json')); 12 | 13 | function makeExternalPredicate(externalArr) { 14 | if (!externalArr.length) { 15 | return () => false; 16 | } 17 | const pattern = new RegExp(`^(${externalArr.join('|')})($|/)`); 18 | return id => pattern.test(id); 19 | } 20 | 21 | function getExternal() { 22 | const external = Object.keys(pkg.peerDependencies || {}); 23 | const allExternal = [...external, ...Object.keys(pkg.dependencies || {})]; 24 | 25 | return makeExternalPredicate(allExternal); 26 | } 27 | 28 | export default { 29 | input: INPUT_FILE, 30 | external: getExternal(), 31 | output: [ 32 | { 33 | file: path.resolve(PACKAGE_ROOT_PATH, 'dist/index.js'), 34 | format: 'cjs', 35 | sourcemap: true, 36 | }, 37 | { 38 | file: path.resolve(PACKAGE_ROOT_PATH, 'dist/index.es.js'), 39 | format: 'es', 40 | sourcemap: true, 41 | }, 42 | ], 43 | plugins: [ 44 | url(), 45 | resolve(), 46 | babel({ 47 | exclude: 'node_modules/**', 48 | }), 49 | typescript({ 50 | tsconfigOverride: { 51 | include: resolve(__dirname, 'lib'), 52 | project: resolve(__dirname, 'tsconfig.json'), 53 | }, 54 | rollupCommonJSResolveHack: true, 55 | useTsconfigDeclarationDir: true, 56 | objectHashIgnoreUnknownHack: true, 57 | clean: true, 58 | }), 59 | commonjs(), 60 | terser(), 61 | ], 62 | }; 63 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "module": "ES2015", 5 | "target": "ES2019", 6 | "lib": ["es6", "dom", "ES2016", "ES2017", "ES2020", "ESNext"], 7 | "sourceMap": true, 8 | "allowJs": false, 9 | "jsx": "react", 10 | "declaration": true, 11 | "declarationDir": "dist", 12 | "moduleResolution": "Node", 13 | "forceConsistentCasingInFileNames": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": false, 17 | "strictNullChecks": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "noUnusedLocals": false, 20 | "noUnusedParameters": true, 21 | "skipLibCheck": true, 22 | "allowSyntheticDefaultImports": true, 23 | "esModuleInterop": true, 24 | "baseUrl": ".", 25 | "outDir": "./dist" 26 | }, 27 | "include": ["lib/*"], 28 | "exclude": ["node_modules", "build", "dist", "__tests__"] 29 | } 30 | --------------------------------------------------------------------------------