How to store client in redux? - reactjs

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.

Related

Who to load dropdown options from API in react JS with typescript and react saga?

Here is my page, Here I want to load brand option from API.
I have written saga attached below:
Action.tsx
export const getBrandsForDropdown = (request: IPagination) => {
return {
type: actions,
payload: request
}
}
Api.tsx
export const getBrandsForDropdown = async () => {
const page = 1;
const limit = 1000;
console.log("get brand drop down");
const query = `user/master/brands?page=${page}&limit=${limit}`;
return client(query, { body: null }).then(
(data) => {
console.log("get brand drop down in ");
return { data, error: null };
},
(error) => {
return { data: null, error };
}
);
};
Reducer.ts
case actions.GET_BRANDS_DROPDOWN_PENDING:
return {
...state,
loading: true,
};
case actions.GET_BRANDS_DROPDOWN_REJECTED:
return {
...state,
loading: false,
};
case actions.GET_BRANDS_DROPDOWN_RESOLVED:
return {
...state,
loading: false,
brandOptions: action.payload,
};
Saga.ts
function* getBrandForDropDownSaga(action: HandleGetBrandsForDropdown) {
yield put(switchGlobalLoader(true));
yield put(pendingViewBrand());
try {
const { data } = yield getBrandsForDropdown();
yield put(resolvedViewBrand(data));
yield put(switchGlobalLoader(false));
} catch (error) {
yield put(switchGlobalLoader(false));
return;
}
}
After this I don't how to call it in my page and get it as a options in brand dropdown
Original Answer: Just Use Thunk
You can do this with redux-saga but I wouldn't recommend it. redux-thunk is a lot easier to use. Thunk is also built in to #reduxjs/toolkit which makes it even easier.
There is no need for an IPagination argument because you are always setting the pagination to {page: 1, limit: 1000}
Try something like this:
import {
createAsyncThunk,
createSlice,
SerializedError
} from "#reduxjs/toolkit";
import { IDropdownOption } from "office-ui-fabric-react";
import client from ???
// thunk action creator
export const fetchBrandsForDropdown = createAsyncThunk(
"fetchBrandsForDropdown",
async (): Promise<IDropdownOption[]> => {
const query = `user/master/brands?page=1&limit=1000`;
return client(query, { body: null });
// don't catch errors here, let them be thrown
}
);
interface State {
brandOptions: {
data: IDropdownOption[];
error: null | SerializedError;
};
// can have other properties
}
const initialState: State = {
brandOptions: {
data: [],
error: null
}
};
const slice = createSlice({
name: "someName",
initialState,
reducers: {
// could add any other case reducers here
},
extraReducers: (builder) =>
builder
// handle the response from your API by updating the state
.addCase(fetchBrandsForDropdown.fulfilled, (state, action) => {
state.brandOptions.data = action.payload;
state.brandOptions.error = null;
})
// handle errors
.addCase(fetchBrandsForDropdown.rejected, (state, action) => {
state.brandOptions.error = action.error;
})
});
export default slice.reducer;
In your component, kill the brandOptions state and access it from Redux. Load the options when the component mounts with a useEffect.
const brandOptions = useSelector((state) => state.brandOptions.data);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchBrandsForDropdown());
}, [dispatch]);
CodeSandbox Link
Updated: With Saga
The general idea of how to write the saga is correct in your code.
take the parent asynchronous action.
put a pending action.
call the API to get data.
put a resolved action with the data or a rejected action with an error.
The biggest mistakes that I'm seeing in your saga are:
Catching errors upstream.
Mismatched data types.
Not wrapping API functions in a call effect.
Error Handling
Your brands.api functions are all catching their API errors which means that the Promise will always be resolved. The try/catch in your saga won't have errors to catch.
If you want to catch the errors in the saga then you need to remove the catch from the functions getBrandsForDropdown etc. You can just return the data directly rather than mapping to { result: data, error: null }. So delete the whole then function. I recommend this approach.
export const getBrandsForDropdown = async () => {
const page = 1;
const limit = 1000;
const query = `user/master/brands?page=${page}&limit=${limit}`;
return client(query, { body: null });
}
If you want to keep the current structure of returning a { result, error } object from all API calls then you need to modify the saga to look for an error in the function return.
function* getBrandForDropDownSaga() {
yield put(switchGlobalLoader(true));
yield put(pendingGetBrands());
const { data, error } = yield call(getBrandsForDropdown);
if (error) {
yield put(rejectedGetBrands(error.message));
} else {
yield put(resolvedGetBrands(data));
}
yield put(switchGlobalLoader(false));
}
Mismatched Data Types
There's some type mismatching in your reducer and state that you need to address. In some places you are using an array IBrand[] and in others you are using an object { results: IBrand[]; totalItems: number; currentPage: string; }. If you add the return type IState to the reducer then you'll see.
There's also a mismatch between a single IBrand and an array. I don't know the exact shape of your API response, but getBrandsForDropdown definitely has an array of brands somewhere. Your saga getBrandForDropDownSaga is dispatching resolvedViewBrand(data) which takes a single IBrand instead of resolvedGetBrands(data) which takes an array IBrand[]. If you add return types to the functions in your brands.api file then you'll see these mistakes highlighted by Typescript.
Don't Repeat Yourself
You can do a lot of combining in your API and your saga between the getBrands and the getBrandsForDropdown. Getting the brands for the dropdown is just a specific case of getBrands where you set certain arguments: { page: 1, limit: 1000 }.
export interface IPagination {
page?: number;
limit?: number;
sort?: "ASC" | "DESC";
column?: string;
}
export const getBrands = async (request: IPagination): Promise<IBrands> => {
const res = await axios.get<IBrands>('/user/master/brands', {
params: request,
});
return res.data;
};
function* coreGetBrandsSaga(request: IPagination) {
yield put(switchGlobalLoader(true));
yield put(pendingGetBrands());
try {
const data = yield call(getBrands, request);
yield put(resolvedGetBrands(data));
} catch (error) {
yield put(rejectedGetBrands(error?.message));
}
yield put(switchGlobalLoader(false));
}
function* getBrandsSaga(action: HandleGetBrands) {
const { sort } = action.payload;
if ( sort ) {
yield put(setSortBrands(sort));
// what about column?
}
const brandsState = yield select((state: AppState) => state.brands);
const request = {
// defaults
page: 1,
limit: brandsState.rowsPerPage,
column: brandsState.column,
// override with action
...action.payload,
}
// the general function can handle the rest
yield coreGetBrandsSaga(request);
}
function* getBrandsForDropDownSaga() {
// handle by the general function, but set certain the request arguments
yield coreGetBrandsSaga({
page: 1,
limit: 1000,
sort: "ASC",
column: "name",
})
}
export default function* brandsSaga() {
yield takeLatest(HANDLE_GET_BRANDS, getBrandsSaga);
yield takeLatest(GET_BRANDS_DROPDOWN, getBrandForDropDownSaga);
...
}
CodeSandbox

How to use an action within reducer?

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.

Custom Websocket in Redux Architecture

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.

React redux dispatch action to connect to mqtt

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;

How to call promise function in reducer?

I am call a promise function in my reducer, so I call it like this:
cart(state, action) {
switch(action.type) {
//...
case LOG_IN:
return getCartProducts(true)
}
}
getCartProducts(isLogin) {
// ....
return cartApi.list({
customerId: "",
items: items
}).then(data => {
return cartReduce(undefined, receiveCartProducts(data.items, false))
})
}
so this just returns a promise, not the object I want to
You should make async actions for these as described in the Redux docs.
import fetch from 'isomorphic-fetch';
// One action to tell your app you're fetching data
export const GET_CART_PRODUCTS_REQUEST = 'GET_CART_PRODUCTS_REQUEST'
function getCartProductsRequest(customerId) {
return {
type: GET_CART_PRODUCTS_REQUEST,
customerId
};
}
// One action to signify success
export const GET_CART_PRODUCTS_SUCCESS = 'GET_CART_PRODUCTS_SUCCESS'
function getCartProductsSuccess(cartProducts) {
return {
type: GET_CART_PRODUCTS_SUCCESS,
cartProducts
};
};
// One action to signify an error
export const GET_CART_PRODUCTS_ERROR = 'GET_CART_PRODUCTS_ERROR'
function getCartProductsSuccess(error) {
return {
type: GET_CART_PRODUCTS_ERROR,
error
};
};
// This is the async action that fires one action when it starts
// And then one of two actions when it finishes
export function fetchCartProducts(customerId) {
return dispatch => {
// Dispatch action that tells your app you're going to get data
// This is where you'll set any 'isLoading' state in your store
dispatch(getCartProductsRequest(customerId))
return fetch('https://api.website.com/customers/' + customerId + '/cartProducts')
.then(response => response.json())
.then(
// Fire success action on success
cartProducts => dispatch(getCartProductsSuccess(cartProducts)),
// Fire error action on error
error => dispatch(getCartProductsError(error))
);
};
};
}

Resources