├── .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 | [](https://www.npmjs.com/package/mqtt-react-hooks)
4 | [](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 |
--------------------------------------------------------------------------------