Reputation: 1962
I'm new to React and I've created a simple hook to allow some components to subscribe to MQTT messages through a global connection to the broker stored in a context. I'm hoping to encapsulate the MQTT bits into this one file and allow my low-level components to read values from it and publish to it while letting the middle layers of the component tree ignore MQTT and do the layout. I'm open to suggestions on the design of this app if redux would be better or anything like that.
I'm experiencing a race condition where my values are only show on some page refreshes but not others. Confusingly, useEffect is not being called more than twice early on and I was expecting it to be called on every page render. Perhaps that's not occurring with each update to the mqtt incoming on('message')
. I'd like it to respond when a message comes in.
Furthermore, annoyingly my mqtt.connect is called about 4 times when I run this, I think because it's trying again so quickly before it actually connects. The if (client == null)
has not changed yet.
src/App.tsx
:
import Welcome from "./components/Welcome"
import ReadOnlyWidget from "./components/ReadOnlyWidget"
import { useMqtt, MqttProvider } from "./MqttContext"
const url = 'ws://10.0.200.10:9001'
export default function App() {
return (
<MqttProvider brokerUrl={url}>
<ReadOnlyWidget topic="/sample/tower-mill/drive/feed" field="feed_distance" />
<ReadOnlyWidget topic="/sample/tower-mill/drive/feed" field="feed_velocity" />
</MqttProvider>
);
}
src/MqttContext.tsx
:
import React from "react"
import mqtt from 'precompiled-mqtt'
import _ from 'lodash'
export const MqttContext = React.createContext(null)
export const MqttProvider = ({ brokerUrl, children }) => {
const [client, setClient] = React.useState(null)
const [messages, setMessages] = React.useState({})
if (client == null) {
const newClient = mqtt.connect(brokerUrl)
newClient.on('connect', () => {
console.log("new client connected")
})
newClient.on('disconnect', () => {
console.log('new client disconnected')
setClient(null)
})
newClient.on('message', (topic, message, packet) => {
const json = JSON.parse(new TextDecoder("utf-8").decode(message))
console.log(json)
setMessages(_.set(messages, topic, json))
})
setClient(newClient)
}
return (
<MqttContext.Provider value={{ client, messages }}>
{children}
</MqttContext.Provider>
)
}
export const useMqtt = ({topic, field}) => {
const mqttContext = React.useContext(MqttContext)
const [value, setValue] = React.useState(null)
mqttContext.client.subscribe(topic)
React.useEffect(() => {
console.log("use effect")
setValue(_.get(mqttContext.messages, [topic, field]))
})
return value
}
src/components/ReadOnlyWidget.tsx
:
import React from 'react';
import { useMqtt } from "../MqttContext"
export default (props) => {
const value = useMqtt({topic: props.topic, field: props.field})
return (
<p>{props.topic} {props.field} {value}</p>
)
}
Upvotes: 0
Views: 282
Reputation: 1962
The answer to this was a actually that no changes were being registered by React due to referential equality vs. structural equality.
setMessages(_.set(messages, topic, json))
We creating a reference identical to the original messages when there was no update. The "equivalent" expression
{...messages, [topic]: json}
Creates a new reference every time. This causes the useEffect to run constantly and always eventually show something regardless of the rendering race conditiion.
However, this is not a create use of useEffect and I have rewritten the hook to add a subscription system to subscribe to the on message updates and directly call into the hook's local state instead. But the above does fix the code and cause what I expected to happen to happen.
Upvotes: 0
Reputation: 582
Your useEffect is missing a dependency array
React.useEffect(() => {
console.log("use effect")
setValue(_.get(mqttContext.messages, [topic, field]))
}, [mqttContext, topic, field, messages]);
useEffect does not get called on every render, but rather execute the callback function (effect) passed to it whenever the value of a variable within the dependency array changes. An empty dependency array will run exactly once (twice in strict mode) when the component mounts, and never again. You said you want it to be called every time a message comes in, so if you take your messages
state variable from MqttContext.tsx would be ideal. Since hooks are functions, however, it's not as simple as passing that as a parameter (since that only gets the initial value). I would instead consider migrating your connection handling into the hook and return multiple values, ie
const thingsToReturn = {
value,
messages
}
return thingsToReturn;
and grab them wherever needed by
const {
value,
messages
} = useMqtt("your params here");
Upvotes: 1