Using socketio in a full stack application
Tags: webdev, websockets, socketio, flasksocketio
Published: 2024-03-10 21:49:29

# 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](https://github.com/Chris10Garcia/phase-4-final-project-crossfit-workout-planner)

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)
```

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 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.