Introduction
socketio
is an event driven library that allows for real-time, bi-directional communication between server and client. It is mostly used for chat applications but for my project, I used it to replace the HTTP server client communication.
For my phase 5 project blog, I decided to share how to implement socketio
with my full stack application
My goal with using socketio
was to replace the communication between the frontend and backend but keep as much of my project logic intact and the same. This means replacing all fetch requests (GET, POST, PATCH, and DELETE) and API routes but still keep how my project processes that data on the frontend and backend.
Setting up socketio
Backend
Flask was a requirement for my phase 5 project so I used the 'flask-socketio' package. I am using a python virtual shell to install my packages, so I ran the following command in terminal: pyenv install flask-socketio
On the server side, you instantiate your flask app and configure it as you normally do, but with flask-socketio
you need to do the following additional things:
- You need to edit your CORS and set
resources={r"/*":{"origins":"*"}}
- You need to insatiate a
socketio
object, pass your app into it, and set the CORS settings - Instead of running
app.run(port=5555, debug=True)
you instead runsocketio.run(app, debug=True, port=5555)
# config file
from flask import Flask
from flask_socketio import SocketIO
from flask_cors import CORS
# Instantiate app
app = Flask(__name__)
# Instantiate CORS
CORS(app, resources={r"/*":{"origins":"*"}})
socketio = SocketIO(app, cors_allowed_origins="*")
# app file
if __name__ == '__main__':
socketio.run(app, debug=True, port=5555)
Now you have a socketio
server running on the backend
Frontend
Let’s set up the frontend. Install the package, npm install socket.io-client
Now to establish your frontend connection, you instantiate a socketio
object
const socket = io("localhost:5555", {
transports: ["websocket"],
cors: { origin: "*",},
}
There are some settings to pass:
- The first is the domain to connect to. We're passing our local host backend address here
- Second, we're passing a dictionary of settings
-
socketio
uses both websockets and http connections, where the latter is a backup. to ensure my project uses websockets, I configured it to only establish that connection. this is optional / dependent on what your project goals are - cors: setting the cors settings
Note that with each instance of the socket object, a new connection is established. I wanted to have one connection so I created this socket in the App
component as a global variable. I then used React context to pass this object directly to child components that receive and send data.
Start your react app and the socketio
connection is now establish.
Testing Your Connection
To ensure and test that your connection works, lets include the following
On the backend app.py
file:
@socketio.on("connect")
def handle_connect():
print("Client connected!")
emit( "connected", {"data": f"id: {request.sid} is connected"})
socketio
for python and flask uses decorators to register events.
One special event is called "connect". Calling socketio.on("connect")
event decorator wrapped around a function will call that function every time a first connection is established.
Here, every time we connect on the backend, we print out to the terminal "client connected".
Next we emit
data back.
emit
is a socketio
function that sends data to the name of the room you are sending that data to. The next argument is the data you're sending. socketio
JSONIFY your data automatically so you do not need to use that function.
So here, we are sending data to the frontend to a room called "connected". Please note, do not use special key word events such as connect or disconnect as it will cause bugs to occur.
On the frontend,
socket.on("connected", (data)=>{
console.log(data)
})
The socket.on
is similar to the backend where the first argument is the name of the room. However, this uses a second argument as a callback function, where the data is passed and you can perform whichever action you want. In our case, we receive the socket id and it gets printed out in the console log. This will indicate to us that the connection was successfully established
Converting my application
In the App
component, a fetch get request is performed within the useEffect
hook to get the data from the backend. I replaced these fetch requests as follows:
original:
fetch("/workout_plans")
.then( r => r.json())
.then( d => setPlans(d))
fetch("/schedules")
.then( r => r.json())
.then( d => setSchClasses(d))
fetch("/coaches")
.then( r => r.json())
.then( d => setCoaches(d))
fetch("/exercise_moves")
.then( r => r.json())
.then( d => setMoves(d))
to:
socket.on("coaches", data => setCoaches(data))
socket.on("schedules", data => setSchClasses(data))
socket.on("workout_plans", data => setPlans(data))
socket.on("exercise_moves", data => setMoves(data))
socketio
is listening to these four rooms for data.
On the backend. I created a function that pulls all of my data from the database. Note, the objects from my DB needs to be serialized. I used flask-marshamallow
and marshamallow
to serialize these DB objects and include or exclude specific relationships and fields. if your data from the backend doesn't have complex relationships that require some type of serialization, you may pass them directly into socketio
.
The data is then emitted to the rooms I specified:
def refresh_all_data():
coaches = Coach.query.all()
workout_plans = Workout_Plan.query.all()
exercise_moves = Exercise_Move.query.all()
schedules = Schedule.query.all()
emit("coaches", coaches_schema.dump(coaches))
emit("workout_plans", workout_plans_schema.dump(workout_plans))
emit("exercise_moves", exercise_moves_schema.dump(exercise_moves))
emit("schedules", schedules_schema.dump(schedules))
Thus, when the frontend and backend first connect, within the on "connect" event, I call on this refresh_all_data()
function. All of the records are pulled, the data is serialized, and it is then emitted to these rooms.
On the frontend, these rooms are being listened to, and when the data is received, the object data is passed to the state variables established, updating the frontend's various views. Since these rooms are within the useEffect
hook, React re-renders again but not infinitely. Everything else in the App
component stays the same.
Next, all components with the suffix Form
utilize fetch requests for patching, or posting data. Also within the ClassScheduleDetail
component, there are fetching delete requests. These components logic are dependent on acknowledgements, and socketio
allows you to provide this when data is emitted and received.
Utilizing CoachForm
as an example, previously when a form data is submitted, there were two routes: if the form was submitting a new object or updating an existing object. Within each of those two routes, if the response was ok, to perform various actions involving refreshing the component and setting the page to show the object or to display the error from the backend as to why it didn't work.
Previously:
function submitData(values){
if (values.id === ""){
fetch("/coaches", {
method: "POST",
headers: {"Content-Type" : "application/json"},
body: JSON.stringify(values)
})
.then( r => {
if (r.ok){
r.json().then(data => {
setRefresh(!refresh)
history.push(`/coaches/${data.id}`)
setFormData(data)
setApiError({})
})
} else {
r.json().then( err => {
setApiError(err)
})
}
})
} else {
fetch(`${values.id}`, {
method : "PATCH",
headers : { "Content-Type" : "application/json"},
body : JSON.stringify(values)
})
.then( r => {
if (r.ok){
r.json().then(data => {
setRefresh(!refresh)
setApiError({})
})
} else {
r.json().then(err => {
setApiError(err)})
}
})
}
}
All of my components that processed data like this all follow a very similar logic pattern. I replaced these fetch requests with the following on the frontend:
function submitData(values){
if (values.id === ""){
socket.emit("new_coach", values, result => {
if (result.ok){
setRefresh(!refresh)
history.push(`/coaches/${result.data.id}`)
setFormData(result.data)
setApiError({})
} else {
setApiError(result.errors)
}
})
} else {
socket.emit("update_coach", values, result => {
if (result.ok){
setRefresh(!refresh)
history.push(`/coaches/${result.data.id}`)
setFormData(result.data)
setApiError({})
} else {
setApiError(result.errors)
}
})
}
}
Above, the socket.emit()
function has three arguments: the name of the room, the data we are transmitting, and an optional acknowledgement. The acknowledgement we have to design and set up on the backend so that my app maintains the same code logic as before.
On the backend, I originally had API routes to handle get / post requests and get / patch / delete requests. Using socketio
, I removed the following:
class CoachesIndex(Resource):
def get(self):
coaches = Coach.query.all()
response = make_response(
coaches_schema.dump(coaches),
200
)
return response
def post(self):
ch_data = request.get_json()
del ch_data["id"]
try:
new_coach = Coach(**ch_data)
db.session.add(new_coach)
db.session.commit()
except Exception as e:
error_message = str(e)
return {"errors" : error_message }, 400
response = make_response(
coach_schema.dump(new_coach),
201
)
return response
I register events that correspond to the frontend's emitting response, one for new objects being created, and another for objects being updated. For the coach path I created the following:
@socketio.on("new_coach")
def handle_new_coach(data):
result = {
"data" : None,
"errors" : {},
"ok" : False
}
del data["id"]
try:
new_coach = Coach(**data)
db.session.add(new_coach)
db.session.commit()
except Exception as e:
error_message = str(e)
result["errors"] = error_message
return result
result["ok"] = True
result["data"] = coach_schema.dump(new_coach)
refresh_all_data()
return result
The data from the frontend can be received as a parameter within your function whereas before the data is received from the request object and pulled using get_json()
function. There are some other changes as well:
- I create a result dictionary. here I mimic some of the response attributes the frontend is dependent on.
- if creating and submitting the data was successful to the DB, I call on that
refresh_all_data()
function. When it runs, it emits data to the rooms I specified however, nothing happens yet... - I return the result dictionary. In socketio for python, acknowledgements is provided through
return
s at the end of yoursocketio.on
function
On the frontend, the component reads if the response is ok. This works similarly to previous way where I cause a change on the dependency array that the useEffect
utilizes (refresh variable). This causes the code within it to run, allowing the rooms to be read, and for my app to update.
Thank you for reading my blog. While not the most common use of socketio
, implementing this technology was fun and interesting. I hope others who are on this journey can utilize some of the things I learned.
Top comments (2)
It's always useful to read implementations of applications for learning and own practice evaluation purposes. Best experience I had in nodejs with socket.io was when using rethinkdb, as it has its own functions that can be used to emit updates when data change is detected. Too bad it's an abandoned anymore db.
Hi,
This is Sourov Pal. I am a freelance web developer and Software Developer. I can do one of project for free. If you like my work you will pay me otherwise you don't need to pay. No upfront needed, no contract needed. If you want to outsource your work to me you may knock me.
My what's app no is: +8801919852044
Github Profile: github.com/sourovpal
Thanks