Websockets are a protocol for creating a persistant connection between a client and a server, allowing data to be sent in either direction outside of the typical request/response cycle. This makes them very useful for features that rely on live updates, like chatting, gameplay, and notifications.
The goal of this guide is the successful implementation of websockets in a React app with a Flask backend, including deployment on Heroku. For the purposes of this demo, we will be creating a very simple application, that allows live chatting with all active users. This guide is not intended to cover all of the functionality that websockets can provide—it's just a starting point.
Features like message persistance, chatrooms, direct messaging, and notifications won't be covered in this guide. However, hopefully the steps here will get you pointed in the right direction, and give you an implementation you can start building on.
As you go through these steps, you will probably find it useful to look at the documentation for the socket.io
JavaScript library that will be used to set up your websockets in your React app, as well as the documentation for the Flask-SocketIO
library that will be used with your Flask server.
Install flask-socketio
in your backend.
Install socket.io-client in your frontend.
Install eventlet
in your backend. (Specifying the version is necessary here since the latest version will not be compatible with your application).
Update the contents of your requirements.txt file to match your Pipfile.lock (run the following command in your terminal to do this automatically).
Replace the final line of your Dockerfile (the CMD
) with this line:
First create a file to hold your SocketIO object, inside your app folder.
Import SocketIO
from flask_socketio
and initialize an instance of a socketio object.
When you instantiate your app, you must set the appropriate cors_allowed_origins
value for the app to work in production. You could use cors_allowed_origins="*"
to allow all origins, which would work both locally and in production, although it would be less secure. It would be better practice to set this value to be the actual url of your heroku app.
However, if we only include our Heroku app on the list of allowed origins, then our websockets won't work when we run the app locally. One solution is to set the allowed origins based on whether the app is running in production or not:
(There are other approaches you could take here, as well, like setting a value for your allowed origins in your .env
file and a separate value in your Heroku config vars, which might be more flexible.)
Let's make a very simple event handler. We can use the @socketio.on()
decorator to handle specific named events. The decorator will wrap around a function that will manage events of that type.
There are some built-in events like "connection"
which we could write handlers for. We can also define custom events. In this case, we want to handle chat events, so we will use the label "chat"
. (On the front-end, we will need to make sure that we use the same value when we emit our events so that they get handled by this function).
When we recieve a chat message, we are going to broadcast that same message back to everyone who is currently connected. (In most applications, you won't need to broadcast to all connected users—instead you may want to set up chatrooms and send messages to the users who are connected to a specific room—see the documentation here).
We can use the broadcast=True
keyword argument when we use the emit
function achieve the desired functionality—that will emit the message to all connected users.
Unlike with a Flask route handler, we will not need to have an actual return
statement—we send messages explicitly using emit
or send
functions.
At this point, your file should look something like this:
In the __init__.py
file in your app folder, you will need to connect your socket instance to your application.
In this simple example, the socket will only be relevent on the Chat
component, so all of our websocket logic will live on a single component. We can create our socket when the component mounts, and then disconnect when the component unmounts. (If you want to use the same socket connection in components throughout your application, you may want to create a context that makes the socket widely available, or even handle it with redux.)
useEffect
The socket will, by default, connect immediately upon creation.
We can create a listener for "chat"
events on our websocket on the same useEffect
where we created our socket. This listener will be active once the component mounts, and it will remain active until it unmounts, which is the behavior we want.
Let's also use state to store all the messages that we recieve so we can display them.
In our return statement, we'll need to include a form that we can submit to send our messages.
Next let's write the sendChat
function that will emit our message through the websocket when the form is submitted.
socket.emit()
to send chat messages through the websocketWhen we send our messages, we want to include a username in addition to the message itself. (In this example, we're including the username with the request so our backend doesn't have to interact with the database to get user information—the backend can just directly broadcast this message to all connected users and it will contain all the information we want to include.)
If we are using our redux store to access the information associated with our current user, we'll use useSelector
. Depending on the structure of the redux store, the statement we use to get our user information might look something like this:
Once we have our user information, we're ready to handle sending chats. We get the message contents from chatInput
(the controlled value of our form input), and we get the username from the value of user
from our redux store. The socket.emit()
method takes a first parameter which is the event name—in this case, "chat". (It needs to match the value in our backend event handler—@socketio.on("chat")
—so that it will get handled by the correct function). The next parameter to the emit
function will be the data we want to send.
In our return statement, we'll map over the messages array and print our the username and message for each chat.
At this point, your Chat
component might look something like this. Make sure to incorporate this component into your application—e.g., if you're using ReactRouter
you'll probably want to add a Route
for rendering this component to your App.js file.
On our Flask back-end, we create a SocketIO
instance from the flask-socketio
package, and used a custom event handler to broadcast all chat messages that are recieved to all connected users.
In our React front-end, we used socket.io-client
to create a websocket instance. We added a listener to our socket to handle events where we recieve new chats. We displayed all the chats we recieved, and used a form to submit new chats.