简体   繁体   中英

Architecture in a react native app using WebSockets

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?

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:

  1. Data
  2. Communication State
  3. 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.

You can create dedicated class for WebSocket and use it everywhere. It's simple, concise and clear approach. Moreover you will have all stuff related to websockets encapsulated in one place! If you wish you can even create singleton out of this class, but the general idea is this:

class WS {
  static init() {
    this.ws = new WebSocket('ws://localhost:5432/wss1');
  }
  static onMessage(handler) {
    this.ws.addEventListener('message', handler);
  }
  static sendMessage(message) {
    // You can have some transformers here.
    // Object to JSON or something else...
    this.ws.send(message);
  }
}

You have only run init somewhere in index.js or app.js :

WS.init();

And now you can loosely send message from any application layer, from any component, from any place:

WS.sendMessage('My message into WebSocket.');

And receive data back from WebSocket:

WS.onMessage((data) => {
  console.log('GOT', data);
  // or something else or use redux
  dispatch({type: 'MyType', payload: data});
});

So you can use it everywhere even in redux in any action or somewhere else!

There are no official guidelines about that. I think using a component is confusing because it will not be rendered, and I guess if you use Redux you want to share the data from websocket anywhere in the application.

You can give the dispatch function to your Websocket manager.

const store = createStore(reducer);

const ws = new WebSocketManager(store.dispatch, store.getState);

And use this.dispatch inside your class methods.

// inside WebSocketManager class
constructor(dispatch, getState) {
    this.dispatch = dispatch;
    this.getState = getState;
}

You can also use middlewares to handle side effects, I think it is the recommended way. There are two great libraries that you can look :

redux-saga

redux-observable

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM