Custom Websocket in Redux Architecture - reactjs

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.

Related

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.

Call redux action within redux-saga inside websocket callback (stomp + sockjs)

I am using redux and redux-saga in my project. Right now using WebSocket I have a problem calling a FETCH_SUCCESS redux action inside a callback of socket response. I tried making the callback a generator as well but didn't work as well.
function* websocketSaga() {
const socket = new SockJS(`${CONFIG.API_URL}/ws`);
const stomp = Stomp.over(socket);
const token = yield select(selectToken);
stomp.connect(
{
Authorization: `Bearer ${token}`,
},
frame => {
stomp.subscribe('/queue/data', message => {
const response = JSON.parse(message.body);
console.log(response); // here is the proper response, it works
put({
type: FETCH_SUCCESS, // here the FETCH_SUCCESS action is not called
payload: response.dataResponse,
});
});
...
....
}
);
}
Or maybe this WebSocket should be implemented in a completely different way in redux-saga?
You won't be able to use yield put inside a callback function. Stompjs knows nothing about sagas, so it doesn't know what it's supposed to do when given a generator function.
The simplest approach, though not necessarily the best, is to go directly to the redux store in the callback, and dispatch the action without involving redux-saga. For example:
import store from 'wherever you setup your store'
// ...
stomp.subscribe('/queue/data', message => {
const response = JSON.parse(message.body);
store.dispatch({
type: FETCH_SUCCESS,
payload: response.dataResponse,
});
});
If you'd like to use a more redux-saga-y approach, I would recommend wrapping the subscription in an event channel. Event channels take a callback-based API and turn it into something that you can interact with using redux-saga's effects such as take
Here's how you might create the event channel:
import { eventChannel } from 'redux-saga';
function createChannel(token) {
return eventChannel(emitter => {
const socket = new SockJS(`${CONFIG.API_URL}/ws`);
const stomp = Stomp.over(socket);
stomp.connect(
{
Authorization: `Bearer ${token}`,
},
frame => {
stomp.subscribe('/queue/data', message => {
const response = JSON.parse(message.body);
emitter(response); // This is the value which will be made available to your saga
});
}
);
// Returning a cleanup function, to be called if the saga completes or is cancelled
return () => stomp.disconnect();
});
}
And then you'd use it like this:
function* websocketSaga() {
const token = yield select(selectToken);
const channel = createChannel(token);
while (true) {
const response = yield take(channel);
yield put({
type: FETCH_SUCCESS,
payload: response.dataResponse,
});
}
}
Promise should be the perfect fit. Just wrap the callback related code in a promise and resolve it in the callback function. After that use the yield to get the data from the promise. I have modified your code with the Promise below.
function* websocketSaga() {
const socket = new SockJS(`${CONFIG.API_URL}/ws`);
const stomp = Stomp.over(socket);
const token = yield select(selectToken);
const p = new Promise((resolve, reject) => {
stomp.connect(
{
Authorization: `Bearer ${token}`,
},
frame => {
stomp.subscribe('/queue/data', message => {
const response = JSON.parse(message.body);
console.log(response); // here is the proper response, it works
resolve(response); // here resolve the promise, or reject if any error
});
...
....
}
);
});
try {
const response = yield p; // here you will get the resolved data
yield put({
type: FETCH_SUCCESS, // here the FETCH_SUCCESS action is not called
payload: response.dataResponse,
});
} catch (ex) {
// handle error here, with rejected value
}
}
I will give you another way of managing this: create a component connected to redux where you will handle the WS subscription. This component will not render anything to the UI but will be useful for handling redux store interactions.
The main idea is, don't put everything into redux-saga, try and split it into multiple parts to make it easier to maintain.
const socket = new SockJS(`${CONFIG.API_URL}/ws`);
function WSConnection(props) {
const {token, fetchDone} = props;
const [stomp, setStomp] = React.useState();
const onMessage = React.useCallback(message => {
const response = JSON.parse(message.body);
fetchDone(response.dataResponse);
}, [fetchDone]);
const onConnect = React.useCallback(frame => {
const subscription = stomp.subscribe('/queue/data', onMessage);
// cleanup subscription
return () => subscription.unsubscribe();
}, [stomp, onMessage]);
const onError = React.useCallback(error => {
// some error happened, handle it here
}, []);
React.useEffect(() => {
const header = {Authorization: `Bearer ${token}`};
stomp.connect(header, onConnect, onError);
// cleanup function
return () => stomp.disconnect();
}, [stomp])
React.useEffect(() => {
setStomp(Stomp.over(socket));
}, []);
return null;
}
const mapStateToProps = state => ({
... // whatever you need from redux store
});
const mapDispatchToProps = dispatch => ({
... // whatever actions you need to dispatch
});
export default connect(mapStateToProps, mapDispatchToProps)(WSConnection);
You can also take it a step further and extract the stomp logic into another file and reuse it wherever you will need it.
It's not wrong to put everything into redux-saga but it's a nice alternative to handle WS connections inside components connected to redux (and easier to understand to people who are not completely familiar with redux-saga and channels etc).
I have the same stack over the years and only recently I faced websockets over Stomp client.
None of the above solutions doesn't work for me both technically and mentally
Reasons:
I don't like channels with Stomp because the only way to manipulate connections in more surgical way you have to use global state object (for me - it's redux). It doesn't seems right even if you storing only random generated IDS (with unsubscribe function it will be... read more here about store serialization
the way with container another pain in the ... (you know where). Again redux and a lot of under-the-hood functionality used without any reason
another way with promises: again without storing helpful connection info and some DI by using promises inside generators. This narrows down the implementation choice
So:
I need to have connection info (I decided to use state but not in: redux, component state. Singleton state). Stomp doesn't force you to place ID but I do because I want to manage connections by myself
I need one entry point without: promises, iterators and a lot of things that will be pain for future-me. One place to "rule them all" (as I want)
- activate: login
- deactivate: logout
- subscribe: componentDidMount
- unsubscribe: componentWillUnmount
DI by request in one place (passing store.dispatch to constructor only if need it) // main topic of the question
And I wrote this implementation that perfectly works for me:
import SockJS from 'sockjs-client';
import {
Client,
IMessage,
messageCallbackType,
StompHeaders,
} from '#stomp/stompjs';
import { Action, Dispatch } from 'redux';
type ConnectionId = string;
interface IServiceConfig {
url: string;
dispatch?: Dispatch;
}
export default class Stomp {
serviceConfig: IServiceConfig = {
dispatch: null,
url: null,
};
ids: ConnectionId[] = [];
stomp: Client;
constructor(config: IServiceConfig) {
this.serviceConfig = { ...config };
this.stomp = new Client();
this.stomp.webSocketFactory = () => {
return (new SockJS(config.url));
};
}
alreadyInQueue = (id: ConnectionId): boolean => {
return Boolean(this.ids.find(_id => id === _id));
};
subscribeByDispatchAction = (
destination: string,
callback: (message: IMessage) => Action,
headers: StompHeaders & {
id: ConnectionId;
},
): void => {
const alreadyInQueue = this.alreadyInQueue(headers.id);
if (!alreadyInQueue) {
this.stomp.subscribe(
destination,
(message) => {
this.serviceConfig.dispatch(callback(message));
},
headers,
);
this.ids.push(headers.id);
return;
}
console.warn(`Already in queue #${headers.id}`);
};
subscribe = (
destination: string,
callback: messageCallbackType,
headers: StompHeaders & {
id: ConnectionId;
},
): void => {
const alreadyInQueue = this.alreadyInQueue(headers.id);
if (!alreadyInQueue) {
this.stomp.subscribe(
destination,
(message) => callback(message),
headers,
);
this.ids.push(headers.id);
this.logState('subscribe');
return;
}
console.warn(`Failed to subscribe over Socks by #${headers.id}`);
};
unsubscribe = (id: ConnectionId, headers?: StompHeaders): void => {
this.stomp.unsubscribe(id, headers);
this.ids.splice(this.ids.indexOf(id), 1);
};
activate = (): void => {
this.stomp.activate();
};
deactivate = (): void => {
if (this.ids.length === 0) {
this.stomp.deactivate();
return;
}
for (let i = 0; i < this.ids.length; i++) {
this.unsubscribe(this.ids[i]);
}
/**
* it seems like it's overkil but
* for me it works only if i do all
* the things as you see below
* - stomp deactivation
* - closing webSockets manually by using native constant // sockjs-client
* - closing webSockets instance by using returned value fron factory
*/
this.stomp.deactivate();
this.stomp.webSocket.close(
this.stomp.webSocket.CLOSED,
);
this.stomp.webSocketFactory().close();
};
getAllIds = (): readonly ConnectionId[] => {
return this.ids;
};
// debug method
logState = (method: string): void => {
/* eslint-disable */
console.group(`Stomp.${method}`);
console.log('this', this);
console.log('this.ids', this.getAllIds());
console.log('this.stomp', this.stomp);
console.groupEnd();
/* eslint-enable */
};
}
My configuration file
import { store } from '~/index';
import Stomp from '~/modules/_Core/services/Stomp';
import appConfig from '~/modules/Common/services/appConfig';
export const StompService = new Stomp({
dispatch: store?.dispatch,
url: `${appConfig.apiV1}/websocket`,
});
I hope that it will help someone

Instantiate a new Howler Object in Action Creators and dispatch actions on events

So I'm trying to build a React.js + Redux audio player.
I'm using the Howler.js Library to process audio from an API.
I'm dispatching actions from the player control buttons and from other component such as a track list component.
When the user clicks on a track in the lis, I'm dispatching a track object to the Player Reducer. I instantiate a new Howl Object here which works but there is some issues here :
Because Howler is fetching the data from an API, this is an async task and it's an anti pattern. I know that, but here I'm not triggering events from Howler so it works, but it's a bit buggy.
I can't use event triggers because it would be async and it's a bad pratice to dispatch actions from the Reducer.
What I would like :
Instanciate a new Howler Object in my Action Creator.
Use the event triggers, e.g : onload() or onloaderror(), to dispatch an other action to tell the player that Howler is ready to play a song or if there was an error.
The code :
Action Creator
export const PLAYER_INITIALIZE = 'PLAYER_INITIALIZE'
export const initialize = () => {
return {
type: PLAYER_INITIALIZE,
}
}
export const PLAYER_SET_TRACK = 'PLAYER_SET_TRACK'
export const setTrack = (trackId) => {
return {
type: PLAYER_SET_TRACK,
}
}
export const PLAYER_PLAY_TRACK = 'PLAYER_PLAY_TRACK'
export const playTrack = () => {
return {
type: PLAYER_PLAY_TRACK,
}
}
Reducer
function PlayerReducer(state = initialPlayerState, action) {
switch (action.type) {
case PlayerActions.PLAYER_SET_TRACK:
if (state.audioObj.state() != 'unloaded')
state.audioObj.unload();
return {
...state,
audioObj: new Howl({
src: API.API_STREAM_TRACK + action.trackId,
html5: true,
preload: true,
onload: () => {
console.log("Track loaded succesfully.");
},
onloaderror: (id, err) => {
console.error("Load Error : " + err);
}
}),
trackMetadata: action.trackMetadata
};
case PlayerActions.PLAYER_PLAY_TRACK:
state.audioObj.play();
return {
...state,
isPlaying: true,
};
[...]
My idea is to instanciate Howler like this :
new Howler({
[ ... some options ],
onload: () => {
dispatch({
type: PLAYER_LOAD_SUCCESS
});
},
onloaderror: () => {
dispatch({
type: PLAYER_LOAD_ERROR
});
}
});
How can I edit this code to create a new Howl Object correctly in the Action Creator and pass it to the player on each track that I want to listen?
Can you give me an example ?
Thanks.
You can add redux-thunk to your app and then be able to dispatch multiple actions from a single action creator. After adding redux-thunk, your action could look like the following:
export const setTrack = (trackId) => {
(dispatch, getState) => {
const { audioObj } = getState();
if (audioObj != null) {
audioObj.unload();
}
dispatch({
audioObj: new Howler({
[ ... some options ],
onload: () => {
dispatch({
type: PLAYER_LOAD_SUCCESS
});
},
onloaderror: () => {
dispatch({
type: PLAYER_LOAD_ERROR
});
}
}),
type: PLAYER_SET_TRACK
});
}
you can move your other async and Howler work into action creators this way and make your reducer only concerned with storing information.

Where and how to add SignalR in redux?

I'm creating a react app which implements SignalR and so far I have my connection and all the listeners in the component where I need them. The problem is that I have action creators in Redux which just make a request and get the response in order to call my server and send the data to all the other clients. Once the server emits the event to all clients, one of my listeners gets the data and calls an action creator which just dispatches an action to refresh my redux state.
I feel like I'm not using the action creators in the right way because I have one action creator which just makes the request and gets the response to return it and it's not changing the state.
If a had the socket connection in the store, I would just have to call one action creator and the logic to emit or listen to socket event, would be in other place.
This is my component,
// --- component.js ---
state = {
connection: null,
};
async componentDidMount() {
// handles any network exception and show the error message
try {
await this.setupConnection();
} catch (error) {
this.showNetworkError(`Whoops, there was an error with your network connection. Please reload the page`);
}
setupConnection = () => {
let { connection } = this.state;
this.setState({
connection: (connection = new HubConnectionBuilder().withUrl(HUB_URL).build()),
});
/**
* LISTENERS that are called from the server via websockets
*/
connection.on('InsertTodo', data => {
// action creator
this.props.add(data);
});
connection.on('UpdateTodo', data => {
// action creator
this.props.update(data);
});
}
createTodo = async todo => {
const { connection} = this.state;
// action creator
const createdTodo = await this.props.createTodo(todo);
if (createdTodo) {
// the below sentence calls the server to emit/send the todo item to all other clients
// and the listener in the setupConnection function is executed
connection.invoke('EmitTodoCreate', createdTodo);
} else {
// there was a problem creating the todo
}
};
This is the action creator
// --- actionCreators.js ----
// ------------------------
export const add = todo => {
return async (dispatch) => {
dispatch({
type: ADD_TODO,
payload: todo,
});
};
};
export const createTodo = todo => {
return async (dispatch) => {
dispatch({
type: START_REQUEST,
});
const response = await postTodo(todo);
const result = await response.json();
if (response.ok) {
dispatch({
type: SUCCESS_REQUEST,
});
// returns the todo item created in order to be sent to the server via websockets
return result;
}
dispatch({
type: FAILURE_REQUEST,
error: result.error,
});
return null;
};
};
I think the best solution is to implement a Redux middleware. It is easy, and you can use authentication for establishing connection, and you can dispatch action creators based on different messages emitted from SignalR.
Per the Redux FAQ, the right place for websockets and other similar connections is in Redux middleware.
This is my custom middleware that establishes the connection, and registers the handlers. Please note that I only would like to receive data, and not interested in sending data. I use REST APIs to send data to server.
import {
JsonHubProtocol,
HttpTransportType,
HubConnectionBuilder,
LogLevel
} from '#aspnet/signalr'; // version 1.0.4
// action for user authentication and receiving the access_token
import { USER_SIGNED_IN } from '../actions/auth';
const onNotifReceived = res => {
console.log('****** NOTIFICATION ******', res);
};
const startSignalRConnection = connection => connection.start()
.then(() => console.info('SignalR Connected'))
.catch(err => console.error('SignalR Connection Error: ', err));
const signalRMiddleware = ({ getState }) => next => async (action) => {
// register signalR after the user logged in
if (action.type === USER_SIGNED_IN) {
const urlRoot = (window.appConfig || {}).URL_ROOT;
const connectionHub = `${urlRoot}/api/service/hub`;
const protocol = new JsonHubProtocol();
// let transport to fall back to to LongPolling if it needs to
const transport = HttpTransportType.WebSockets | HttpTransportType.LongPolling;
const options = {
transport,
logMessageContent: true,
logger: LogLevel.Trace,
accessTokenFactory: () => action.user.access_token
};
// create the connection instance
const connection = new HubConnectionBuilder()
.withUrl(connectionHub, options)
.withHubProtocol(protocol)
.build();
// event handlers, you can use these to dispatch actions to update your Redux store
connection.on('OperationProgress', onNotifReceived);
connection.on('UploadProgress', onNotifReceived);
connection.on('DownloadProgress', onNotifReceived);
// re-establish the connection if connection dropped
connection.onclose(() => setTimeout(startSignalRConnection(connection), 5000));
startSignalRConnection(connection);
}
return next(action);
};
export default signalRMiddleware;
And inside my store.js file
import signalRMiddleware from '../middlewares/signalRMiddleware';
...
createStore(rootReducer, {}, composeEnhancers(applyMiddleware(signalRMiddleware)));

How to store client in redux?

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.

Resources