So I am making a connection to a MQTT broker via Redux. I have three actions, one making the connection, another one checking for error and one receiving the message.
Only the first one gets triggered and the other 2 do not trigger. The connection is successful.
Here is my code:
Actions
export const mqttConnectionInit = (topic) => {
return {
type: 'INIT_CONNECTION',
topic:topic
}
}
export const mqttConnectionState = (err = null) => {
return {
type: 'MQTT_CONNECTED',
payload: err
}
}
export const processMessage = (data) => dispatch => {
console.log('Receiving Message')
return {
type: 'MESSAGE_RECEIVED',
payload: data
}
}
Reducer
import { mqttConnectionState} from './mqttActions'
import { processMessage} from './mqttActions'
const initState = {
client: null,
err: null,
message : 'message'
}
const createClient = (topic) => {
const mqtt = require('mqtt')
const client = mqtt.connect('ws://localhost:9001');
client.on('connect', function () {
mqttConnectionState('MQTT_CONNECTED')
client.subscribe(topic, (err, granted) => {
if (err) alert(err)
console.log(`Subscribed to: ` + topic)
console.log(granted)
});
});
//messages recevied during subscribe mode will be output here
client.on('message', function (topic, message) {
// message is Buffer
console.log(message.toString())
processMessage({topic, message})
// client.end() will stop the constant flow of values
})
return client;
}
const mqttReducer = (state = initState, action) =>{
switch (action.type) {
case 'INIT_CONNECTION':
return {
...state,
client: createClient(action.topic)
}
case 'MQTT_CONNECTED':
return {
...state,
err: action.payload
}
case 'MESSAGE_RECEIVED':
return {
...state,
message: action.payload //payload:data
}
default:
return state
}
}
export default mqttReducer
Why mqttConnectionState and processMessage do not get triggered?
You can never call async logic from within a reducer! Your createClient method is entirely async logic, and so it cannot go in a reducer.
In addition, you should not put non-serializable values into the Redux store.
Instead, we recommend that persistent connections like sockets should go into middleware.
Related
Basically i wanted to integrate the mqtt in my react app. I want to connect to the mqtt when user login and disconnect when user logs out.
Firstly i created actions,reducers to dispatch the connection state and get client payload. It's connecting but the mqtt servers seems to be reconnecting in a loop with few secs. I want it to connect once and disconnect user logs out automatically. I globally initiated the mqtt client in the mqqtActions.js.
import { CONNECT_MQTT_FAILURE, CONNECT_MQTT_REQUEST, CONNECT_MQTT_SUCCESS, DISCONNECT_MQTT_FAILURE, DISCONNECT_MQTT_REQUEST, DISCONNECT_MQTT_SUCCESS } from "../constants/mqttConstants"
import mqtt from 'mqtt'
const mqttHost = "host ip address here";
const mqttPort = 8083;
const mqttUrl = `ws://${mqttHost}:${mqttPort}/mqtt`
const mqttOptions = {
keepalive: 30,
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
reconnectPeriod: 1000,
connectTimeout: 30 * 1000,
clientId: "bharath",
will: {
topic: 'WillMsg',
payload: 'Connection Closed abnormally..!',
qos: 0,
retain: false
},
rejectUnauthorized: false
};
const mqttClient = mqtt.connect(mqttUrl, mqttOptions)
export const mqttConnect = () => async (dispatch) => {
try {
dispatch({
type: CONNECT_MQTT_REQUEST
})
dispatch({
type: CONNECT_MQTT_SUCCESS,
payload: mqttClient
})
//mqttClient.connect()
localStorage.setItem('mqttClient', JSON.stringify(mqttClient))
} catch (error) {
dispatch({
type: CONNECT_MQTT_FAILURE,
payload: error.response && error.response.data.message ? error.response.data.message : error.message
})
}
}
export const mqttDisconnect = () => (dispatch) => {
try {
dispatch({ type: DISCONNECT_MQTT_REQUEST })
mqttClient.end()
localStorage.removeItem('mqttClient')
dispatch({
type: DISCONNECT_MQTT_SUCCESS,
})
} catch (error) {
dispatch({
type: DISCONNECT_MQTT_FAILURE,
payload: error.response && error.response.data.message ? error.response.data.message : error.message
})
}
}
I set up my reducer file like this:
import { CONNECT_MQTT_FAILURE, CONNECT_MQTT_REQUEST, CONNECT_MQTT_SUCCESS, DISCONNECT_MQTT_FAILURE, DISCONNECT_MQTT_REQUEST, DISCONNECT_MQTT_SUCCESS } from "../constants/mqttConstants"
export const connectMqttReducer = (state = {}, action) => {
switch (action.type) {
case CONNECT_MQTT_REQUEST:
return { status: 'connecting' }
case CONNECT_MQTT_SUCCESS:
return { status: 'connected', client: action.payload }
case CONNECT_MQTT_FAILURE:
return { status: 'connect', error: action.payload }
default:
return state
}
}
export const disconnectMqttReducer = (state = {}, action) => {
switch (action.type) {
case DISCONNECT_MQTT_REQUEST:
return { status: 'connected' }
case DISCONNECT_MQTT_SUCCESS:
return { status: 'connect' }
case DISCONNECT_MQTT_FAILURE:
return { status: 'connected', error: action.payload }
default:
return state
}
}
Doing this i'm able to connect but its timestamp when connectedAt is changing continously.And also mqtt.connect() is not function error is also showing. I commented it out. I want to connect it once and disconnect when user login and logout actions are triggered.
Please try the following:
1- Add the following to your environment file:
MQTT_HOST="host ip address here"
MQTT_PORT=8083
2- Add the following in your Constants.js file:
export const mqttUrl = `ws://${process.env.MQTT_HOST}:${process.env.MQTT_PORT}/mqtt`;
export const mqttOptions = {
keepalive: 30,
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
reconnectPeriod: 1000,
connectTimeout: 30 * 1000,
clientId: 'bharath',
will: {
topic: 'WillMsg',
payload: 'Connection Closed abnormally..!',
qos: 0,
retain: false,
},
rejectUnauthorized: false,
};
export const localStorageKeys = {
mqttClient: 'mqttClient',
};
3- Create a new file which will hold all mqtt functionality /src/clients/MqttClient.js:
import mqtt from 'mqtt';
import { localStorageKeys, mqttOptions, mqttUrl } from '#/js/constants/Helpers';
let instance = null;
class MqttClient {
constructor() {
if (!instance) {
instance = this;
}
return instance;
}
myMqtt;
connect() {
this.myMqtt = mqtt.connect(mqttUrl, mqttOptions);
return new Promise((resolve, reject) => {
this.myMqtt.on('connect', () => {
//add instance to local storage if it's not present (if you need it)
const localStorageMqtt = localStorage.getItem(localStorageKeys.mqttClient);
if (localStorageMqtt === null) {
localStorage.setItem(localStorageKeys.mqttClient, JSON.stringify(this.myMqtt));
}
resolve();
});
this.myMqtt.on('error', (error) => reject(error));
});
}
disconnect() {
return new Promise((resolve) => {
this.myMqtt.end(false, {}, () => {
this.myMqtt = null;
//if you added it to the localstorage (on connect)
localStorage.removeItem(localStorageKeys.mqttClient);
resolve();
});
});
}
subscribe(event) {
return new Promise((resolve, reject) => {
if (!this.myMqtt) return reject('No mqtt connection.');
return this.myMqtt.subscribe(event, (err) => {
// Optional callback that you can use to detect if there's an error
if (err) {
console.error(err);
return reject(err);
}
return resolve();
});
});
}
publish(event, data) {
return new Promise((resolve, reject) => {
if (!this.myMqtt) return reject('No mqtt connection.');
return this.myMqtt.publish(event, data, {}, (err) => {
// Optional callback that you can use to detect if there's an error
if (err) {
console.error(err);
return reject(err);
}
return resolve();
});
});
}
on(event, fun) {
// No promise is needed here, but we're expecting one in the middleware.
return new Promise((resolve, reject) => {
if (!this.myMqtt) return reject('No mqtt connection.');
this.myMqtt.on(event, fun);
resolve();
});
}
}
export default MqttClient;
4- Create a new mqtt middleware in your store directory /src/store/middleWares/MqttMiddleWare.js:
export const mqttMiddleWare =
(mqtt) =>
({ dispatch, getState }) =>
(next) =>
(action) => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
/*
* Mqtt middleware usage.
* promise: (mqtt) => mqtt.connect()
* type: 'mqtt' //always (mqtt)
* types: [REQUEST, SUCCESS, FAILURE]
*/
const { promise, type, types, ...rest } = action;
if (type !== 'mqtt' || !promise) {
// Move on! Not a mqtt request or a badly formed one.
return next(action);
}
const [REQUEST, SUCCESS, FAILURE] = types;
next({ ...rest, type: REQUEST });
return promise({ mqtt, dispatch, getState })
.then((result) => {
return next({ ...rest, result, type: SUCCESS });
})
.catch((error) => {
console.log(error);
return next({ ...rest, error, type: FAILURE });
});
};
5- Update your store config to accept mqtt client as an argument then pass it to the mqtt middleware as follows /src/store/configureStore.js:
const middlewares = [];
// log redux data in development mode only
if (process.env.NODE_ENV !== 'production') {
const { logger } = require('redux-logger');
middlewares.push(logger);
}
const configureStore = (mqttClient) => {
const store = createStore(
rootReducer,
/* preloadedState, */
composeWithDevTools(
applyMiddleware(thunkMiddleware, mqttMiddleWare(mqttClient), ...middlewares)
)
);
return store;
};
export default configureStore;
6- Instantiate your mqtt client in /src/index.jsx and pass it to your store:
const mqttClient = new MqttClient();
const store = configureStore(mqttClient);
7- Update your reducer as follows:
import { CONNECT_MQTT_FAILURE, CONNECT_MQTT_REQUEST, CONNECT_MQTT_SUCCESS, DISCONNECT_MQTT_FAILURE, DISCONNECT_MQTT_REQUEST, DISCONNECT_MQTT_SUCCESS } from "../constants/mqttConstants"
export const connectMqttReducer = (state = {}, action) => {
switch (action.type) {
case CONNECT_MQTT_REQUEST:
return { connectionStatus: 'connecting' }
case CONNECT_MQTT_SUCCESS:
return { connectionStatus: 'connected' }
case CONNECT_MQTT_FAILURE:
return { connectionStatus: 'connect failed', error: action.error }
default:
return state
}
}
export const disconnectMqttReducer = (state = {}, action) => {
switch (action.type) {
case DISCONNECT_MQTT_REQUEST:
return { disconnectionStatus: 'disconnecting' }
case DISCONNECT_MQTT_SUCCESS:
return { disconnectionStatus: 'disconnected' }
case DISCONNECT_MQTT_FAILURE:
return { disconnectionStatus: 'connect failed', error: action.error }
default:
return state
}
}
8- Update your actions as follows:
Connect action:
export const startMqttConnection = () => ({
type: 'mqtt',
types: [CONNECT_MQTT_REQUEST, CONNECT_MQTT_SUCCESS, CONNECT_MQTT_FAILURE],
promise: ({ mqtt }) => mqtt.connect(),
});
Disconnect action:
export const stopMqttConnection = () => ({
type: 'mqtt',
types: [DISCONNECT_MQTT_REQUEST, DISCONNECT_MQTT_SUCCESS, DISCONNECT_MQTT_FAILURE],
promise: ({ mqtt }) => mqtt.disconnect(),
});
9- dispatch the required action as follows:
useEffect(() => {
dispatch(startMqttConnection());
return () => {
if (connectionStatus === 'connected') {
dispatch(stopMqttConnection());
}
};
//eslint-disable-next-line
}, [dispatch]);
I'm fetch some data from my API and it correctly works. But when a double dispatch on the same page the API doesn't work anymore. It's better code to explain it:
Server:
router.get("/", (req, res) => {
let sql = "SELECT * FROM design_categories";
let query = connection.query(sql, (err, results) => {
if (err) throw err;
res.header("Access-Control-Allow-Origin", "*");
res.send(results);
});
});
router.get("/", (req, res) => {
let sql = "SELECT * FROM food_categories";
let query = connection.query(sql, (err, results) => {
if (err) throw err;
res.header("Access-Control-Allow-Origin", "*");
res.send(results);
});
});
They work.
action.js
export const fetchDesignCat = () => {
setLoading()
return async dispatch => {
const response = await axios
.get("http://localhost:5000/api/designcategories")
.then(results => results.data)
try {
await dispatch({ type: FETCH_DESIGN_CAT, payload: response })
} catch (error) {
console.log("await error", error)
}
}
}
export const fetchFoodCat = () => {
setLoading()
return async dispatch => {
const response = await axios
.get("http://localhost:5000/api/foodcategories")
.then(results => results.data)
try {
await dispatch({ type: FETCH_FOOD_CAT, payload: response })
} catch (error) {
console.log("await error", error)
}
}
}
Both of them work perfectly.
reducer.js
const initalState = {
db: [],
loading: true,
designcat: [],
foodcat: [],
}
export default (state = initalState, action) => {
switch (action.type) {
// different cases
case FETCH_DESIGN_CAT:
return {
designcat: action.payload,
loading: false,
}
case FETCH_FOOD_CAT:
return {
food: action.payload,
loading: false,
}
}
The reducer updates the states perfectly.
Page settings.js
const Settings = ({ designcat, foodcat, loading }) => {
const dispatch = useDispatch()
// ... code
useEffect(() => {
dispatch(fetchDesignCat()) // imported action
dispatch(fetchFoodCat()) // imported action
// eslint-disable-next-line
}, [])
// ... code that renders
const mapStateToProps = state => ({
designcat: state.appDb.designcat,
foodcat: state.appDb.foodcat,
loading: state.appDb.loading,
})
export default connect(mapStateToProps, { fetchDesignCat, fetchFoodCat })(
Settings
)
Now there's the problem. If I use just one dispatch it's fine I get one or the other. But if I use the both of them look like the if the second overrides the first. This sounds strange to me.
From my ReduxDevTools
For sure I'm mistaking somewhere. Any idea?
Thanks!
Your reducer does not merge the existing state with the new state, which is why each of the actions just replace the previous state. You'll want to copy over the other properties of the state and only replace the ones your specific action should replace. Here I'm using object spread to do a shallow copy of the previous state:
export default (state = initalState, action) => {
switch (action.type) {
case FETCH_DESIGN_CAT:
return {
...state, // <----
designcat: action.payload,
loading: false,
}
case FETCH_FOOD_CAT:
return {
...state, // <----
food: action.payload,
loading: false,
}
}
}
Since the code is abbreviated, I'm assuming you're handling the default case correctly.
As an additional note, since you're using connect with the Settings component, you don't need to useDispatch and can just use the already connected action creators provided via props by connect:
const Settings = ({
designcat,
foodcat,
loading,
fetchDesignCat,
fetchFoodCat,
}) => {
// ... code
useEffect(() => {
fetchDesignCat();
fetchFoodCat();
}, [fetchDesignCat, fetchFoodCat]);
// ... code that renders
};
There's also a race condition in the code which may or may not be a problem to you. Since you start both FETCH_DESIGN_CAT and FETCH_FOOD_CAT at the same time and both of them set loading: false after finishing, when the first of them finishes, loading will be false but the other action will still be loading its data. If this case is known and handled in code (i.e., you don't trust that both items will be present in the state if loading is false) that's fine as well.
The solution to that would be either to combine the fetching of both of these categories into one thunk, or create separate sub-reducers for them with their own loading state properties. Or of course, you could manually set and unset loading.
The more I read about this subject it seems like going down a rabbit hole. This is a new Trading application which receives realtime data through web sockets which is based on a request-response paradigm. There are three separate SPA's in which apart from initial load, every user action triggers a call to the dataStore with a new MDXQuery. So in turn I would need to make fresh subscriptions on a componentDidMount() as well as in the respective ActionCreators.I would like to streamline the code to avoid duplicate code and redundancy.
The below code helps establish a new subscription channel to streams the response through web-socket.(Unlike, most sockets.io code where it comes with a designated open,close,send)
this.subscription = bus.channel(PATH, { mode: bus.wsModes.PULL }).createListener(this.onResponse.bind(this));
this.subscription.subscribe(MDXQuery);
If I read the REDUX documentation as to where should I place the web socket code? It mentions to create a custom middleware.
LINK: https://redux.js.org/faq/codestructure#where-should-websockets-and-other-persistent-connections-live
But I am not very sure how could I go about using this custom web socket code framing my own middleware or doing at the component level would help to mimic this strategy.
const createMySocketMiddleware = (url) => {
return storeAPI => {
let socket = createMyWebsocket(url);
socket.on("message", (message) => {
storeAPI.dispatch({
type : "SOCKET_MESSAGE_RECEIVED",
payload : message
});
});
return next => action => {
if(action.type == "SEND_WEBSOCKET_MESSAGE") {
socket.send(action.payload);
return;
}
return next(action);
}
}
}
Any design inputs would really help!!
I wrote that FAQ entry and example.
If I understand your question, you're asking about how to dynamically create additional subscriptions at runtime?
Since a Redux middleware can see every dispatched action that is passed through the middleware pipeline, you can dispatch actions that are only intended as commands for a middleware to do something. Now, I'm not sure what an MDXQuery is, and it's also not clear what you're wanting to do with the messages received from these subscriptions. For sake of the example, I'll assume that you want to either dispatch Redux actions whenever a subscription message is received, or potentially do some custom logic with them.
You can write a custom middleware that listens for actions like "CREATE_SUBSCRIPTION" and "CLOSE_SUBSCRIPTION", and potentially accepts a callback function to run when a message is received.
Here's what that might look like:
// Add this to the store during setup
const subscriptionMiddleware = (storeAPI) => {
let nextSubscriptionId = 0;
const subscriptions = {};
const bus = createBusSomehow();
return (next) => (action) => {
switch(action.type) {
case "CREATE_SUBSCRIPTION" : {
const {callback} = action;
const subscriptionId = nextSubscriptionId;
nextSubscriptionId++;
const subscription = bus.channel(PATH, { mode: bus.wsModes.PULL })
.createListener((...args) => {
callback(dispatch, getState, ...args);
});
subscriptions[subscriptionId] = subscription;
return subscriptionId;
}
case "CLOSE_SUBSCRIPTION" : {
const {subscriptionId} = action;
const subscription = subscriptions[subscriptionId];
if(subscription) {
subscription.close();
delete subscriptions[subscriptionId];
}
return;
}
}
}
}
// Use over in your components file
function createSubscription(callback) {
return {type : "CREATE_SUBSCRIPTION", callback };
}
function closeSubscription(subscriptionId) {
return {type : "CLOSE_SUBSCRIPTION", subscriptionId};
}
// and in your component:
const actionCreators = {createSubscription, closeSubscription};
class MyComponent extends React.Component {
componentDidMount() {
this.subscriptionId = this.props.createSubscription(this.onMessageReceived);
}
componentWillUnmount() {
this.props.closeSubscription(this.subscriptionId);
}
}
export default connect(null, actionCreators)(MyComponent);
I tried out your solution for my own problem which involves creating a socket instance only when a user is logged and here is how my code looks:
const socketMiddleWare = url => store => {
const socket = new SockJS(url, [], {
sessionId: () => custom_token_id
});
return next => action => {
switch (action.type) {
case types.USER_LOGGED_IN:
{
socket.onopen = e => {
console.log("Connection", e.type);
store.dispatch({
type: types.TOGGLE_SOCK_OPENING
});
if (e.type === "open") {
store.dispatch({
type: types.TOGGLE_SOCK_OPENED
});
createSession(custom_token_id, store);
const data = {
type: "GET_ACTIVE_SESSIONS",
JWT_TOKEN: Cookies.get("agentClientToken")
};
store.dispatch({
type: types.GET_ACTIVE_SESSIONS,
payload: data
});
}
};
socket.onclose = () => {
console.log("Connection closed");
store.dispatch({
type: types.POLL_ACTIVE_SESSIONS_STOP
});
// store.dispatch({ type: TOGGLE_SOCK_OPEN, payload: false });
};
socket.onmessage = e => {
console.log(e)
};
if (
action.type === types.SEND_SOCKET_MESSAGE
) {
socket.send(JSON.stringify(action.payload));
return;
} else if (action.type === types.USER_LOGGED_OUT) {
socket.close();
}
next(action);
}
default:
next(action);
break;
}
};
};
It doesn't work though but could you point me in the right direction. Thanks.
I am building an react application to connect to and display data from a MQTT server.
I have implemented the basic connection code in mqtt/actions.js See below:
const client = mqtt.connect(options);
client.on('connect', function () {
mqttConnectionState('MQTT_CONNECTED')
client.subscribe(['btemp', 'otemp'], (err, granted) => {
if (err) alert(err)
console.log(`Subscribed to: otemp & btemp topics`)
})
})
client.on('message', function (topic, message) {
updateTemp({topic: topic, value: message.toString()})
});
const mqttConnectionState = (action, err = null) => {
return {
type: action,
payload: err
}
}
I am looking to on button press initiate the mqtt connection and then dispatch a connection success event.
However with the above code I am unsure exactly how this would work.
I could move the connect line const client = mqtt.connect(options); to a function and run that function on button click but then then the client.on functions will not be able to see the client const.
How is best to approach this?
I am using React.JS, Redux and the MQTT.JS libraries.
Update: Trying to dispatch and action when a message is received
Reducer:
const createClient = () => {
const client = mqtt.connect(options);
client.on('connect', function () {
mqttConnectionState('MQTT_CONNECTED')
client.subscribe(['btemp', 'otemp'], (err, granted) => {
if (err) alert(err)
console.log(`Subscribed to: otemp & btemp topics`)
});
});
client.on('message', (topic, message) => {
console.log('message received from mqtt')
processMessage({topic, message})
})
return client;
}
case MESSAGE_RECEIVED:
console.log('message received')
messageReceived(payload)
return state;
Actions:
export const processMessage = (data) => dispatch => {
console.log('Processing Message')
return {
type: 'MESSAGE_RECEIVED',
payload: data
}
}
message received from mqtt log each time a message arrives, however processMessage({topic, message}) never executes as Processing Message never logs to the console
"Actions are payloads of information that send data from your application to your store" (docs)
So you have to create the client in the Reducer (his function). Put it on the Redux state like this:
initialState = {
client: null
}
and you reducer.js file should look like this:
import {
mqttConnectionState
} from './actions'
let initialState = {
client: null ,
err: null
}
const createClient = () => {
const client = mqtt.connect(options);
client.on('connect', function () {
mqttConnectionState('MQTT_CONNECTED')
client.subscribe(['btemp', 'otemp'], (err, granted) => {
if (err) alert(err)
console.log(`Subscribed to: otemp & btemp topics`)
});
});
return client;
}
function app(state = initialState, action) {
switch (action.type) {
case 'INIT_CONNECTION':
return {
...state,
client: createClient()
})
case 'MQTT_CONNECTED':
return {
...state,
err: action.payload
})
default:
return state
}
}
and you actions.js:
...
const mqttConnectionInit = () => {
return {
type: 'INIT_CONNECTION'
}
}
const mqttConnectionState = (err = null) => {
return {
type: 'MQTT_CONNECTED',
payload: err
}
}
...
this way you can dispatch the action mqttConnectionInit in the onclick button event.
Dispatching Redux actions inside a reducer may clear your store. I mean, setting it's state to what you have under initialState. And require cycles are no-op.
Yesterday I did some changes in my code, because I've tried solution above and ended up with a warning "require cycles are allowed but can result in uninitialized values". I moved mqtt connection related code into the middleware.
import { DEVICE_TYPE, HOST, PASSWORD, PORT, USER_NAME } from '../utils/variables';
import { mqttConnectionInit, mqttConnectionState } from '../actions/devices';
import mqtt from 'mqtt/dist/mqtt';
import { SIGNED_IN } from '../constants/types';
const MqttMiddleware = store => next => action => {
if (action.type == SIGNED_IN) {
store.dispatch(mqttConnectionInit());
const client = mqtt.connect(`ws://${HOST}:${PORT}`, { username: USER_NAME, password: PASSWORD });
client.on('connect', function () {
let license = store.getState().auth.license;
store.dispatch(mqttConnectionState(client));
client.subscribe(`/${USER_NAME}/${license}/+/${DEVICE_TYPE}/#`);
});
client.on('message', ((topic, payload) => {
const device = JSON.parse(message(topic, payload.toString()));
console.log(device);
}));
}
next(action);
};
export function message(message, value) {
const msg = message.split('/');
return JSON.stringify({
"id": msg[3],
"user": msg[1],
"license": msg[2],
"type": msg[4],
"name": msg[5],
"value": value == "0" ? 0 : (value.match(/[A-Za-z]/) ? value : Number(value))
});
}
export default MqttMiddleware;
You can do pretty much all you want with the store.
actions.js
import { INIT_CONNECTION, MQTT_CONNECTED } from '../constants/types'
export const mqttConnectionInit = () => {
return {
type: INIT_CONNECTION
}
}
export const mqttConnectionState = (client, err = null) => {
return {
type: MQTT_CONNECTED,
error: err,
client: client,
}
}
reducers.js
import { INIT_CONNECTION, MQTT_CONNECTED } from '../constants/types';
const mqttReducer = (state = initialState, action) => {
switch (action.type) {
case INIT_CONNECTION:
return {
...state,
client: null,
};
case MQTT_CONNECTED:
return {
...state,
err: action.error,
client: action.client,
};
default:
return state;
}
}
const initialState = {
client: null,
err: null,
}
export default mqttReducer;
I'm setting up a redux application that needs to create a client. After initialization, the client has listeners and and APIs that will need to be called based on certain actions.
Because of that I need to keep an instance of the client around. Right now, I'm saving that in the state. Is that right?
So I have the following redux action creators, but then when I want to send a message I need to call the client.say(...) API.
But where should I get the client object from? Should I retrieve the client object from the state? My understanding is that that's a redux anti-pattern. What's the proper way to do this with redux?
Even stranger – should the message send be considered an action creator when it doesn't actually mutate the state?
The actions:
// actions.js
import irc from 'irc';
export const CLIENT_INITIALIZE = 'CLIENT_INITIALIZE';
export const CLIENT_MESSAGE_RECEIVED = 'CLIENT_MESSAGE_RECEIVED';
export const CLIENT_MESSAGE_SEND = 'CLIENT_MESSAGE_SEND';
export function messageReceived(from, to, body) {
return {
type: CLIENT_MESSAGE_RECEIVED,
from: from,
to: to,
body: body,
};
};
export function clientSendMessage(to, body) {
client.say(...); // <--- where to get client from?
return {
type: CLIENT_MESSAGE_SEND,
to: to,
body: body,
};
};
export function clientInitialize() {
return (dispatch) => {
const client = new irc.Client('chat.freenode.net', 'react');
dispatch({
type: CLIENT_INITIALIZE,
client: client,
});
client.addListener('message', (from, to, body) => {
console.log(body);
dispatch(messageReceived(from, to, body));
});
};
};
And here is the reducer:
// reducer.js
import { CLIENT_MESSAGE_RECEIVED, CLIENT_INITIALIZE } from '../actions/client';
import irc from 'irc';
export default function client(state: Object = { client: null, channels: {} }, action: Object) {
switch (action.type) {
case CLIENT_MESSAGE_RECEIVED:
return {
...state,
channels: {
...state.channels,
[action.to]: [
// an array of messages
...state.channels[action.to],
// append new message
{
to: action.to,
from: action.from,
body: action.body,
}
]
}
};
case CLIENT_JOIN_CHANNEL:
return {
...state,
channels: {
...state.channels,
[action.channel]: [],
}
};
case CLIENT_INITIALIZE:
return {
...state,
client: action.client,
};
default:
return state;
}
}
Use middleware to inject the client object into action creators! :)
export default function clientMiddleware(client) {
return ({ dispatch, getState }) => {
return next => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
const { promise, ...rest } = action;
if (!promise) {
return next(action);
}
next({ ...rest });
const actionPromise = promise(client);
actionPromise.then(
result => next({ ...rest, result }),
error => next({ ...rest, error }),
).catch((error) => {
console.error('MIDDLEWARE ERROR:', error);
next({ ...rest, error });
});
return actionPromise;
};
};
}
Then apply it:
const client = new MyClient();
const store = createStore(
combineReducers({
...
}),
applyMiddleware(clientMiddleware(client))
);
Then you can use it in action creators:
export function actionCreator() {
return {
promise: client => {
return client.doSomethingPromisey();
}
};
}
This is mostly adapted from the react-redux-universal-hot-example boilerplate project. I removed the abstraction that lets you define start, success and fail actions, which is used to create this abstraction in action creators.
If your client is not asynchronous, you can adapt this code to simply pass in the client, similar to how redux-thunk passes in dispatch.