Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

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 implementsocketio with myfull stack application

My goal with usingsocketio 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 upsocketio

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 withflask-socketio you need to do the following additional things:

  • You need to edit your CORS and setresources={r"/*":{"origins":"*"}}
  • You need to insatiate asocketio object, pass your app into it, and set the CORS settings
  • Instead of runningapp.run(port=5555, debug=True) you instead runsocketio.run(app, debug=True, port=5555)
# config filefrom flask import Flaskfrom flask_socketio import SocketIOfrom flask_cors import CORS# Instantiate appapp = Flask(__name__)# Instantiate CORSCORS(app, resources={r"/*":{"origins":"*"}})socketio = SocketIO(app, cors_allowed_origins="*")# app fileif __name__ == '__main__':    socketio.run(app, debug=True, port=5555)
Enter fullscreen modeExit fullscreen mode

Now you have asocketio 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 asocketio object

const socket = io("localhost:5555", {transports: ["websocket"],            cors: { origin: "*",},}
Enter fullscreen modeExit 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 theApp 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 thesocketio connection is now establish.

Testing Your Connection

To ensure and test that your connection works, lets include the following

On the backendapp.py file:

@socketio.on("connect")def handle_connect():    print("Client connected!")    emit( "connected", {"data": f"id: {request.sid} is connected"})
Enter fullscreen modeExit fullscreen mode

socketio for python and flask uses decorators to register events.

One special event is called "connect". Callingsocketio.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 weemit data back.

emit is asocketio 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 modeExit fullscreen mode

Thesocket.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 theApp component, a fetch get request is performed within theuseEffect 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 modeExit 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 usedflask-marshamallow andmarshamallow 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 intosocketio.

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 modeExit fullscreen mode

Thus, when the frontend and backend first connect, within the on "connect" event, I call on thisrefresh_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 theuseEffect hook, React re-renders again but not infinitely. Everything else in theApp component stays the same.

Next, all components with the suffixForm utilize fetch requests for patching, or posting data. Also within theClassScheduleDetail component, there are fetching delete requests. These components logic are dependent on acknowledgements, andsocketio allows you to provide this when data is emitted and received.

UtilizingCoachForm 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 modeExit 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 modeExit fullscreen mode

Above, thesocket.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. Usingsocketio, 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 modeExit 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 modeExit 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 usingget_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 thatrefresh_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 throughreturns 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 theuseEffect 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 ofsocketio, 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)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
netplayer profile image
NetPlayer
World should be borderless and open sourced
  • Location
    Greece
  • Work
    Software author at my desk
  • Joined

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.

CollapseExpand
 
sourovpal profile image
Sourov Pal
Full Stack Software Engineer
  • Joined

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

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Mechanical engineer attending Flatiron School's Software Engineering Bootcamp.
  • Location
    New York City
  • Education
    Stony Brook University
  • Pronouns
    He / His / Him
  • Work
    Mechanical Engineer at the MTA New York City Transit
  • Joined

More fromChris

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp