Design for Websocket with React + Redux - reactjs

I am trying to implement Websocket communication with React + Redux.
The question is kind of design of scope.
The following code is part of source code I wrote.
In this page,
1. Init Socket Settings ⇒
2. Start Socket ⇒
3. Execute Event ⇒
4. Receive Socket
When I execute event, I can receive the websocket data, however I have no idea how to locate these method for the best.
My question is I would like to know the best practice of design.
Of corse, I would like to make use of the design of Redux.
If you have better idea, it doesn't matter to use other modules like reducer or actions.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { Socket } from 'phoenix';
//Init Socket Settings
const socket = new Socket("/socket", {params: {token: window.userToken}});
socket.connect();
const channel = socket.channel("member:lobby", {});
//Receive Socket
channel.on('new_message', state => {
console.log("Receive Socket!!!" + state.body);
});
class Sample extends Component{
constructor(props) {
super(props);
this.state = {
data: [],
}
this.doEnterKey = this.doEnterKey.bind(this);
}
componentDidMount() {
//Start Socket
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
}
//Execute Event
doEnterKey(e){
if(e.keyCode === 13){
console.log('Enter Key!!!' + e.target.value);
channel.push("new_message", {body: e.target.value});
}
}
render() {
return (
<div>
<div>Websocket</div>
<div id="messages"></div>
<input id="chat-input" type="text" onKeyDown={this.doEnterKey}></input>
</div>
);
}
}
export default connect()(Sample);
Where is the best place to location for socket and channel?

Here if you need to store web socket details to the redux.
Create action and reducer
Call the action on the receive function with response data.
Update the reducer.

Related

Redux-saga: Best way to handle async call data used in only one component

I need advice concerning redux-saga and the way to handle async call. I don't find anwsers to my questions.
I would like to know how can I handle properly async API call which return data used in only one component (so useless to store it in the store) ?
In my react application, I use redux-saga to handle async call. When the saga finish correctly, I dispatch a success action which store result in the store.
However, i find useless to store the result when I only want to display it in one component. Instead I would like to run a saga and return by a callback data to my component without storing it int the store. Is it possible ? How can I do that ?
thanks.
Here is a sample code for you, that code makes an api request in componentDidMount lifecycle and sets the data to its state and after it renders it.
import React, { Component } from "react";
import { render } from "react-dom";
import axios from 'axios';
class App extends Component {
constructor() {
super();
this.state = {
data: []
};
}
async componentDidMount() {
try {
let response = await axios.get('https://jsonplaceholder.typicode.com/users');
console.log('response.data: ', response.data);
this.setState({
data: response.data
});
} catch (error) {
console.log('error: ', error);
}
}
render() {
return (
<ul>
{this.state.data.map(item => <li>{item.name}</li>)}
</ul>
);
}
}
Hope this helps.

How can I maintain state and "this" with react component functions

Currently, when socket.io emits a 'gmessage' from my server, and the socket in my component catches it, my entire state is replaced.
So the current flow is like this:
I emit a message from the component, it goes to my server, then to the watson API. The API sends a response to my server, and the server sends that to the component.
So that first message creates the connection to socket.io, then the second socket.io event is caught on the same connection.
Is there a better way to set up the connection to socket.io and handle both the emit and the "on gmessage" parts of this? Thank you for any tips in advance. I'm still new to react, so anything you see that I should do differently is helpful!!
...
import {Launcher} from 'react-chat-window'
import io from 'socket.io-client';
import { getUser } from '../actions/userActions';
class Workshop extends Component {
constructor() {
super();
this.state = {
messageList: []
};
}
_onMessageWasSent(message) {
this.setState({
messageList: [message, ...this.state.messageList]
})
var socket = io.connect('http://localhost:5000');
socket.emit('message', { message });
var myThis = this
var myState = this.state
socket.on('gmesssage', function (data) {
console.log(data);
myThis.setState({
messageList: [{
author: 'them',
type: 'text',
data: data
}, ...myState.messageList]
})
})
}
render() {
return (
<Grid container className="DashboardPage" justify="center">
<Grid item xs={12}>
<div>Welcome to your Workshop</div>
<TeamsTaskLists />
</Grid>
<Launcher
agentProfile={{
teamName: 'Geppetto',
imageUrl: 'https://geppetto.ai/wp-content/uploads/2019/03/geppetto-chat-avi.png'
}}
onMessageWasSent={this._onMessageWasSent.bind(this)}
messageList={this.state.messageList}
showEmoji
/>
</Grid>
);
}
}
const mapStatetoProps = state => ({
user: state.user
});
export default connect(
mapStatetoProps,
{ getUser }
)(Workshop);
Fareed has provided a working Redux solution and suggested improvements for a non-Redux approach, so I'd like to address the issues in your current code:
You're reinitializing the socket variable and creating its listener every time a new message is received instead of configuring and initializing the socket object just once inside a separate file and then consuming it by your Workshop component and possibly other components across your project.
You're appending the newly received message by calling setState({ messages: [...] }) twice in _onMessageWasSent.
To solve the first issue, you can create a separate file and move your socket related imports and initialization to it like:
// socket-helper.js
import io from 'socket.io-client';
export const socket = io.connect('http://localhost:5000');
Bear in mind, this is a minimal configuration, you can tweak your socket object before exporting it as much as the socket.io API allows you to.
Then, inside the Workshop component's componentDidMount subscribe to this socket object like:
import { socket } from 'path/to/socket-helper.js';
class Workshop extends Component {
constructor(props) {
super(props);
...
}
componentDidMount() {
// we subscribe to the imported socket object just once
// by using arrow functions, no need to assign this to myThis
socket.on('gmesssage', (data) => {
this._onMessageWasSent(data);
})
}
...
}
React docs say
If you need to load data from a remote endpoint, this is a good place to instantiate the network request.
componentDidMount will run only once, so your listener will not get redefined multiple times.
To solve the second issue, _onMessageWasSent handler function should only receive the new message and append it to previous messages using setState, like this:
_onMessageWasSent(data) {
this.setState(previousState => ({
messageList: [
{
author: 'them',
type: 'text',
data: data,
},
...previousState.messageList,
]
}))
}
Since you are already using redux it's better to make socket.io dispatch redux actions for you, you can use redux saga same as this article describes or you can use this implementation without saga but you need to have redux-thunk middleware :
1- Create a file lets say lib/socket.js
then inside this file create socket.io client lets call it socket and create initSocketIO function where you can fire any redux actions in response to socket.io:
import socket_io from "socket.io-client";
// our socket.io client
export const socket = socket_io("http://localhost:5000");
/**
* init socket.io redux action
* #return {Function}
*/
export const initSocketIO = () => (dispatch, getState) => {
// connect
socket.on('connect', () => dispatch(appActions.socketConnected()));
// disconnect
socket.on('disconnect', () => dispatch(appActions.socketDisconnected()));
// message
socket.on('message', message => {
dispatch(conversationActions.addOrUpdateMessage(message.conversationId, message.id, message));
socket.emit("message-read", {conversationId: message.conversationId});
});
// message
socket.on('notifications', ({totalUnread, notification}) => {
dispatch(recentActions.addOrUpdateNotification(notification));
dispatch(recentActions.setTotalUnreadNotifications(totalUnread));
});
};
2- then inside your App component call this action once your App is mounted:
import React, {Component} from "react";
import {initSocketIO} from "./lib/socket.js";
export class App extends Component {
constructor(props) {
super(props);
this.store = configureStore();
}
componentDidMount() {
// init socket io
this.store.dispatch(initSocketIO());
}
// ... whatever
}
3- now you can also fire any socket.io action from any component using this:
import React, {Component} from "react";
import {socket} from "./lib/socket.js"
MyComponent extends Components {
myMethod = () => socket.emit("message",{text: "message"});
}
or from any redux action using thunk for example:
export const sendMessageAction = (text) => dispatch => {
socket.emit("my message",{text});
dispatch({type: "NEW_MESSAGE_SENT",payload:{text}});
}
Notes:
1-This will perfectly work, but still, I prefer the redux-saga method for managing any side effects like API and socket.io.
2-In your components, you don't need to bind your component methods to this just use arrow functions for example:
myMethod = () => {
// you can use "this" here
}
render = (){
return <Launcher onMessageWasSent={this.myMethod} />
}
Depending on your application, I think Redux would be overkill for this.
I would change to a lambda expression here though to avoid having to save this references:
socket.on('gmesssage', data => {
console.log(data);
this.setState({
messageList: [{
author: 'them',
type: 'text',
data: data
}, ...this.messageList]
})
})
You might consider using the new React Hooks also e.g. turn it into a stateless component and use useState:
const Workshop = () => {
const [messageList, setMessageList] = useState([]);
...

Phoenix + React Native and Redux: Where should I put channel object?

I have a component called PlatformMain that currently depends on a global channel object from Phoenix defined inside of the component's file.
let channel;
let socket = new Socket("...", {params: {token: window.userToken}});
socket.connect();
class PlatformMain extends React.Component {
componentWillMount() {
this.connectUser();
}
connectUser() {
const { user } = this.props;
channel = socket.channel("user_pool:" + user.email, { app: APP });
this.setupChannel();
}
setupChannel() {
channel.join()
.receive("ok", () => { console.log("Successfully joined call channel") })
.receive("error", () => { console.log("Unable to join") })
channel.on("match_found", payload => {
...
});
...
}
If the user presses a button, I'd like that to dispatch an action as well as push a message to the channel.
onPress() {
console.log("APPROVE_MATCH");
const { peer, waitForResponse } = this.props;
approveMatch(peer);
channel.push("approve_match", { // <------ want to put this somewhere else
"matched_client_email": peer.email,
});
}
My question is, if I want to "reduxify" the channel.push call, where should I put it? It feels weird not having the channel.push(...) somewhere else since it's an API call. I was going to put it in a saga using redux-saga like so:
function* approveMatch(action) {
const peer = action.payload.peer;
channel.push("approve_match", { // <------- but how would I get the same channel object?
"matched_client_email": peer.email,
});
}
export default function* watchMatchingStatus() {
yield takeEvery(matchingStatusActions.APPROVE_MATCH, approveMatch);
}
But wouldn't I need to point to the same channel object? How would I do that? If I put the initialization of the channel in its own file and I export and import it in multiple places, wouldn't it execute the file multiple times (and consequently join the channel multiple times)?
You could put the initialization of channel in its own file and safely import multiple times, the evaluation of the module only happens once. You can check the spec for confirmation:
Do nothing if this module has already been evaluated. Otherwise, transitively evaluate all module dependences of this module and then evaluate this module.

Invariant Violation: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch

I am using ALT for my ReactJS project. I am getting the cannot 'dispatch' error if the ajax call is not yet done and I switch to another page.
Mostly, this is how my project is setup. I have action, store and component. I querying on the server on the componentDidMount lifecycle.
Action:
import alt from '../altInstance'
import request from 'superagent'
import config from '../config'
import Session from '../services/Session'
class EventActions {
findNear(where) {
if (!Session.isLoggedIn()) return
let user = Session.currentUser();
request
.get(config.api.baseURL + config.api.eventPath)
.query(where)
.set('Authorization', 'Token token=' + user.auth_token)
.end((err, res) => {
if (res.body.success) {
this.dispatch(res.body.data.events)
}
});
}
}
export default alt.createActions(EventActions)
Store
import alt from '../altInstance'
import EventActions from '../actions/EventActions'
class EventStore {
constructor() {
this.events = {};
this.rsvp = {};
this.bindListeners({
findNear: EventActions.findNear
});
}
findNear(events) {
this.events = events
}
}
export default alt.createStore(EventStore, 'EventStore')
Component
import React from 'react';
import EventActions from '../../actions/EventActions';
import EventStore from '../../stores/EventStore';
import EventTable from './tables/EventTable'
export default class EventsPage extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: true,
events: [],
page: 1,
per: 50
}
}
componentDidMount() {
EventStore.listen(this._onChange.bind(this));
EventActions.findNear({page: this.state.page, per: this.state.per});
}
componentWillUnmount() {
EventStore.unlisten(this._onChange);
}
_onChange(state) {
if (state.events) {
this.state.loading = false;
this.setState(state);
}
}
render() {
if (this.state.loading) {
return <div className="progress">
<div className="indeterminate"></div>
</div>
} else {
return <div className="row">
<div className="col m12">
<h3 className="section-title">Events</h3>
<UserEventTable events={this.state.events}/>
</div>
</div>
}
}
}
componentDidMount() {
EventStore.listen(this._onChange.bind(this));
EventActions.findNear({page: this.state.page, per: this.state.per});
}
This would be my will guess. You are binding onChange which will trigger setState in _onChange, and also an action will be fired from findNear (due to dispatch). So there might be a moment where both are updating at the same moment.
First of all, findNear in my opinion should be as first in componentDidMount.
And also try to seperate it in 2 differnet views (dumb and logic one, where first would display data only, while the other one would do a fetching for example). Also good idea is also to use AltContainer to actually avoid _onChange action which is pretty useless due to the fact that AltContainer has similar stuff "inside".
constructor() {
this.events = {};
this.rsvp = {};
this.bindListeners({
findNear: EventActions.findNear
});
}
findNear(events) {
this.events = events
}
Also I would refactor this one in
constructor() {
this.events = {};
this.rsvp = {};
}
onFindNear(events) {
this.events = events
}
Alt has pretty nice stuff like auto resolvers that will look for the action name + on, so if you have action called findNear, it would search for onFindNear.
I can't quite see why you'd be getting that error because the code you've provided only shows a single action.
My guess however would be that your component has been mounted as a result of some other action in your system. If so, the error would then be caused by the action being triggered in componentDidMount.
Maybe try using Alt's action.defer:
componentDidMount() {
EventStore.listen(this._onChange.bind(this));
EventActions.findNear.defer({page: this.state.page, per: this.state.per});
}
I believe it's because you're calling an action, and the dispatch for that action only occurs when after the request is complete.
I would suggest splitting the findNear action into three actions, findNear, findNearSuccess and findNearFail.
When the component calls findNear, it should dispatch immediately, before even submitting the reuqest so that the relevant components will be updated that a request in progress (e.g. display a loading sign if you like)
and inside the same action, it should call the other action findNearSuccess.
The 'Fetching Data' article should be particularly helpful.

How do I handle Store's listeneres in ReactJS?

Here is the average Store:
// StoreUser
import EventEmitter from 'eventemitter3';
import Dispatcher from '../dispatcher/dispatcher';
import Constants from '../constants/constants';
import React from 'react/addons';
import serverAddress from '../utils/serverAddress';
import socket from './socket';
var CHANGE_EVENT = "change";
var storedData = {
userName: null
}
socket
.on('newUserName', (newUserName)=>{
storedData.userName = newUserName;
AppStore.emitChange();
})
function _changeUserName (newUserName) {
socket.emit('signUp', newUserName)
}
var AppStore = React.addons.update(EventEmitter.prototype, {$merge: {
emitChange(){
// console.log('emitChange')
this.emit(CHANGE_EVENT);
},
addChangeListener(callback){
this.on(CHANGE_EVENT, callback)
},
removeChangeListener(callback){
this.removeListener(CHANGE_EVENT, callback)
},
getStoredData(callback) {
callback(storedData);
},
dispatcherIndex:Dispatcher.register(function(payload){
var action = payload.action;
switch(action.actionType){
case Constants.CHANGE_USER_NAME:
_changeUserName(payload.action.actionArgument);
break;
}
return true;
})
}});
export default AppStore;
Here is average component, connected to the store:
import React from 'react';
import StoreUser from '../../stores/StoreUser';
import Actions from '../../actions/Actions';
export default class User extends React.Component {
constructor(props) {
super(props);
this.state = {
userName: null,
};
}
componentDidMount() {
StoreUser.addChangeListener(this._onChange.bind(this));
}
componentWillUnmount() {
StoreUser.removeChangeListener();
}
render() {
const { userName } = this.state;
return (
<div key={userName}>
{userName}
</div>
);
}
_onChange(){
StoreUser.getStoredData(_callbackFunction.bind(this))
function _callbackFunction (storedData) {
// console.log('storedData', storedData)
this.setState({
userName: storedData.userName,
})
}
}
}
This was working absolutely fine. But now, I'm facing quite big mistake in this approach:
WebApp has, say, sidebar with user's name, which is listening to StoreUser in order to display up to date userName. And also, the webapp has a component (page), which contains user's profile (including userName), which is also listening to StoreUser. If I will unmount this component, then the sidebar will stop listening to StoreUser too.
I've learned this approach from egghead.io or some other tutorial - I don't remember, where exactly. Seems, I've missed some important point about how to manage store listeners. Could you, please, suggest simple approach to solve the described issue?
I'm trying to stick to the original Flux architecture, please, don't advice me any alternative implementations of Flux architecture. This is the only issue I have.
The issue is that you aren't removing the your listener when you unmount. You have to be very careful to remove exactly the same function reference that you added (passing this.callback.bind(this) twice doesn't work, since the two calls return different function references). The simplest approach would be:
//...
export default class User extends React.Component {
constructor(props) {
super(props);
this.state = {
userName: null,
};
// create fixed bound function reference we can add and remove
// (like the auto-binding React.createComponent does for you).
this._onChange = this._onChange.bind(this);
}
componentDidMount() {
StoreUser.addChangeListener(this._onChange);
}
componentWillUnmount() {
// make sure you remove your listener!
StoreUser.removeChangeListener(this._onChange);
}
//...
There are a couple of oddities with your code:
Why does StoreUser.getStoredData take a callback, rather than just returning the data? With flux, you should always query synchronously. If data needs fetching, the getter could fire off the ajax fetch, return null, and then trigger another action when the ajax request returns, causing another emitChange and hence ultimately triggering the re-render with the user data.
It looks like your store is triggering server writes. Normally, in flux, this would be the responsibility of an action creator rather than the store, though I don't think it's a major violation for the store to do it.

Resources