Issue
I have a React Native app I'm going to be building that uses WebSockets. I have a WebSocket library written in JavaScript and I'm simply re-using it for this project, which is fantastic.
My question is, being new to React/React Native, what is the best practice for setting up and maintaining all of the traffic going through the WebSocket?
Initially my idea was to create the websocket in the main App component, something like this:
export default class App extends Component {
constructor(props) {
super(props);
this.ws = new WebSocket;
}
componentWillMount() {
console.log(this.ws);
}
render() {
console.log("We are rendering the App component.....");
return (
<View style={styles.container}>
<Text style={styles.welcome}>Hello, world</Text>
</View>
);
}
}
The actual WebSocket class would contain all of the respective connection handling:
ws.onopen = () => {
// connection opened
ws.send('something'); // send a message
};
ws.onmessage = (e) => {
// a message was received
console.log(e.data);
};
ws.onerror = (e) => {
// an error occurred
console.log(e.message);
};
ws.onclose = (e) => {
// connection closed
console.log(e.code, e.reason);
};
My question is, since the data coming through WebSocket will be applicable for state through many components in the React Native app, but it is not a class that will extend React.Component
, do I not interact with Redux in the WebSocket
class? Do I move all of the WebSocket connection handling to the App
component and dispatch actions there to Redux?
What's the common pattern here to instantiate my WebSocket
class and ensure that all traffic in it is properly getting passed to Redux so all component's state will funnel correctly?
Solution
Great answers here so far. Just wanted to add that where you keep your data should really be a decision based on what type of data it is. James Nelson has an excellent article on this topic that I refer to regularly.
For your case, let's talk about the first 3 types of state:
- Data
- Communication State
- Control State
Data
Your WebSocket connection is generic and could technically return anything, but it's likely that the messages you're receiving are data. For example, let's say you're building a chat app. Then, the log of all messages that have been sent and received would be the data. You should store this data in redux with a messages
reducer:
export default function messages(state = [], action) {
switch (action.type) {
case 'SEND_MESSAGE':
case 'RECEIVE_MESSAGE': {
return [ ...state, action.message ];
}
default: return state;
}
}
We don't have to (and we shouldn't) have any WebSocket logic in our reducers, as they are generic and don't care where the data is coming from.
Also, note that this reducer is able to handle sending and receiving in exactly the same way. This is because the network communication is handled separately by our communication state reducer.
Communication State
Since you're using WebSockets, the types of communication state you want to track may differ from my example. In an app that uses a standard API, I would track when a request is loading, failed, or successful.
In our chat app example, you'll probably want to track these request states whenever you send a message, but there could be other things you want to classify as communication state as well.
Our network
reducer can use the same actions as the messages
reducer:
export default function network(state = {}, action) {
switch (action.type) {
case 'SEND_MESSAGE': {
// I'm using Id as a placeholder here. You'll want some way
// to tie your requests with success/failure receipt.
return {
...state,
[action.id]: { loading: true }
};
} case 'SEND_MESSAGE_SUCCESS': {
return {
...state,
[action.id]: { loading: false, success: true }
};
} case 'SEND_MESSAGE_FAILURE': {
return {
...state,
[action.id]: { loading: false, success: false }
};
}
default: return state;
}
}
This way, we can easily find the status of our requests, and we don't have to bother with loading/success/failure in our components.
However, you might not care about the success/failure of any given request since you're using WebSockets. In that case, your communication state might just be whether or not your socket is connected. If that sounds better to you, then just write a connection
reducer that responds to actions on open/close.
Control State
We'll also need something to initiate the sending of messages. In the chat app example, this is probably a submit button that sends whatever text is in an input field. I won't demonstrate the whole component, as we'll use a controlled component.
The takeaway here is that the control state is the message before it's sent. The interesting bit of code in our case is what to do in handleSubmit
:
class ChatForm extends Component {
// ...
handleSubmit() {
this.props.sendMessage(this.state.message);
// also clear the form input
}
// ...
}
const mapDispatchToProps = (dispatch) => ({
// here, the `sendMessage` that we're dispatching comes
// from our chat actions. We'll get to that next.
sendMessage: (message) => dispatch(sendMessage(message))
});
export default connect(null, mapDispatchToProps)(ChatForm);
So, that addresses where all of our state goes. We've created a generic app that could use actions to call fetch
for a standard API, get data from a database, or any number of other sources. In your case, you want to use WebSockets
. So, that logic should live in your actions.
Actions
Here, you'll create all of your handlers: onOpen
, onMessage
, onError
, etc. These can still be fairly generic, as you've already got your WebSocket utility set up separately.
function onMessage(e) {
return dispatch => {
// you may want to use an action creator function
// instead of creating the object inline here
dispatch({
type: 'RECEIVE_MESSAGE',
message: e.data
});
};
}
I'm using thunk for the async action here. For this particular example, that might not be necessary, but you'll probably have cases where you want to send a message then handle success/failure and dispatch multiple actions to your reducers from within a single sendMessage
action. Thunk is great for this case.
Wiring It All Together
Finally, we have everything set up. All we have to do now is initialize the WebSocket and set up the appropriate listeners. I like the pattern Vladimir suggested--setting up the socket in a constructor--but I would parameterize your callbacks so that you can hand in your actions. Then your WebSocket class can set up all the listeners.
By making the WebSocket class a singleton, you're able to send messages from inside your actions without needing to manage references to the active socket. You'll also avoid polluting the global namespace.
By using the singleton set up, whenever you call new WebSocket()
for the first time, your connection will be established. So, if you need the connection to be opened as soon as the app starts, I would set it up in componentDidMount
of App
. If a lazy connection is okay, then you can just wait until your component tries to send a message. The action will create a new WebSocket and the connection will be established.
Answered By - Luke Willis
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.