DEV Community

Chris
Chris

Posted on

Using socketio in a full stack application

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 run socketio.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)
Enter fullscreen mode Exit fullscreen mode

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: "*",},
}
Enter fullscreen mode Exit fullscreen mode

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"})
Enter fullscreen mode Exit fullscreen mode

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)
      })
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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)})
        }
      })
    }
  }
Enter fullscreen mode Exit fullscreen mode

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)
        }
      })
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 returns at the end of your socketio.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)

Collapse
 
sourovpal profile image
Sourov Pal

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

Collapse
 
netplayer profile image
NetPlayer

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.