├── .gitattributes ├── .gitignore ├── README.md ├── main.py ├── react-server-side-events ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── reportWebVitals.js │ └── setupTests.js └── yarn.lock └── requirements.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | react-server-side-events/public/index.html linguist-vendored 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Demo app to send server-side events from python (fastapi) and receive events using React. 2 | 3 | Server Sent Events are a standard allowing browser clients to receive a stream of updates from a server over a HTTP connection without resorting to polling. Unlike WebSockets, Server Sent Events are a one way communications channel - events flow from server to client only. 4 | 5 | Server-Sent Events (SSE) is often overshadowed by its two big brothers — Web Sockets and Long-Polling. However, there are many practical use cases for using SSE. Updating dynamic content, sending push notifications, and streaming data in Real-time are just a few of the applications that SSE can be utilized for. We implement a simple SSE application with FastAPI and React. 6 | 7 | > Traditionally, a web page has to send a request to the server to receive new data; that is, the page requests data from the server. With server-sent events, it’s possible for a server to send new data to a web page at any time, by pushing messages to the web page. 8 | 9 | You can think of SSE as a unidirectional websocket. Only the server can send messages to subscribed clients. There are many web applications where web sockets maybe overkill. For example, updating the price of an item on a product page does not need bidirectional communication. The server simply needs one-way communication to update prices for all of its clients. This is a perfect use case for SSE. 10 | 11 | In short, SSE is a great tool for streaming quick real-time data. They offer unidirectional communication from a server to its clients and are typically used for updating dynamic content on web pages. 12 | 13 | We will be using python and FastAPI. FastAPI is a great tool for SSE applications as it is really easy to use and is built upon starlette which has SSE capabilities built in. 14 | 15 | When working with Server Sent Events, communications between client and server are initiated by the client (browser). The client creates a new JavaScript EventSource object, passing it the URL of an endpoint which is expected to return a stream of events over time. 16 | 17 | The server receives a regular HTTP request from the client (these should pass through firewalls etc like any other HTTP request which can make this method work in situations where WebSockets may be blocked). The client expects a response with a series of event messages at arbitrary times. The server needs to leave the HTTP response open until it has no more events to send, decides that the connection has been open long enough and can be considered stale, or until the client explicitly closes the initial request. 18 | 19 | Every time that the server writes an event to the HTTP response, the client will receive it and process it in a listener callback function. 20 | 21 | ## Client Side 22 | 23 | ```javascript 24 | const evtSource = new EventSource("http://127.0.0.1:8000/stream"); 25 | 26 | useEffect(() => { 27 | evtSource.addEventListener("new_message", function (event) { 28 | // Logic to handle status updates 29 | setMessage((messages) => [...messages, event.data]); 30 | }); 31 | 32 | evtSource.addEventListener("end_event", function (event) { 33 | setMessage((messages) => [...messages, event.data]); 34 | evtSource.close(); 35 | }); 36 | 37 | return () => { 38 | evtSource.close(); 39 | }; 40 | }, []); 41 | ``` 42 | 43 | ## Server side 44 | 45 | ```python 46 | from sse_starlette.sse import EventSourceResponse 47 | 48 | @app.get("/stream") 49 | async def message_stream(request: Request): 50 | async def event_generator(): 51 | while True: 52 | if await request.is_disconnected(): 53 | logger.debug("Request disconnected") 54 | break 55 | 56 | # Checks for new messages and return them to client if any 57 | counter, exists = get_message() 58 | if exists: 59 | yield { 60 | "event": "new_message", 61 | "id": "message_id", 62 | "retry": MESSAGE_STREAM_RETRY_TIMEOUT, 63 | "data": f"Counter value {counter}", 64 | } 65 | else: 66 | yield { 67 | "event": "end_event", 68 | "id": "message_id", 69 | "retry": MESSAGE_STREAM_RETRY_TIMEOUT, 70 | "data": "End of the stream", 71 | } 72 | 73 | await asyncio.sleep(MESSAGE_STREAM_DELAY) 74 | 75 | return EventSourceResponse(event_generator()) 76 | ``` 77 | 78 | Check out the code to get the working example. 79 | 80 | Blog post: https://fictionally-irrelevant.vercel.app/posts/why-you-should-use-server-side-events-over-web-sockets-and-long-polling 81 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from fastapi import FastAPI, Request 4 | from fastapi.middleware.cors import CORSMiddleware 5 | from sse_starlette.sse import EventSourceResponse 6 | import asyncio 7 | import uvicorn 8 | 9 | logger = logging.getLogger() 10 | 11 | MESSAGE_STREAM_DELAY = 1 # second 12 | MESSAGE_STREAM_RETRY_TIMEOUT = 15000 # milisecond 13 | app = FastAPI() 14 | 15 | # add CORS so our web page can connect to our api 16 | app.add_middleware( 17 | CORSMiddleware, 18 | allow_origins=["*"], 19 | allow_credentials=True, 20 | allow_methods=["*"], 21 | allow_headers=["*"], 22 | ) 23 | 24 | COUNTER = 0 25 | 26 | 27 | def get_message(): 28 | global COUNTER 29 | COUNTER += 1 30 | return COUNTER, COUNTER < 21 31 | 32 | 33 | @app.get("/stream") 34 | async def message_stream(request: Request): 35 | async def event_generator(): 36 | while True: 37 | if await request.is_disconnected(): 38 | logger.debug("Request disconnected") 39 | break 40 | 41 | # Checks for new messages and return them to client if any 42 | counter, exists = get_message() 43 | if exists: 44 | yield { 45 | "event": "new_message", 46 | "id": "message_id", 47 | "retry": MESSAGE_STREAM_RETRY_TIMEOUT, 48 | "data": f"Counter value {counter}", 49 | } 50 | else: 51 | yield { 52 | "event": "end_event", 53 | "id": "message_id", 54 | "retry": MESSAGE_STREAM_RETRY_TIMEOUT, 55 | "data": "End of the stream", 56 | } 57 | 58 | await asyncio.sleep(MESSAGE_STREAM_DELAY) 59 | 60 | return EventSourceResponse(event_generator()) 61 | 62 | 63 | if __name__ == "__main__": 64 | uvicorn.run(app, host="127.0.0.1", port=8000) 65 | -------------------------------------------------------------------------------- /react-server-side-events/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /react-server-side-events/README.md: -------------------------------------------------------------------------------- 1 | ## Demo app to send server-side events from python (fastapi) and receive events using React. 2 | 3 | Server Sent Events are a standard allowing browser clients to receive a stream of updates from a server over a HTTP connection without resorting to polling. Unlike WebSockets, Server Sent Events are a one way communications channel - events flow from server to client only. 4 | 5 | Server-Sent Events (SSE) is often overshadowed by its two big brothers — Web Sockets and Long-Polling. However, there are many practical use cases for using SSE. Updating dynamic content, sending push notifications, and streaming data in Real-time are just a few of the applications that SSE can be utilized for. We implement a simple SSE application with FastAPI and React. 6 | 7 | > Traditionally, a web page has to send a request to the server to receive new data; that is, the page requests data from the server. With server-sent events, it’s possible for a server to send new data to a web page at any time, by pushing messages to the web page. 8 | 9 | You can think of SSE as a unidirectional websocket. Only the server can send messages to subscribed clients. There are many web applications where web sockets maybe overkill. For example, updating the price of an item on a product page does not need bidirectional communication. The server simply needs one-way communication to update prices for all of its clients. This is a perfect use case for SSE. 10 | 11 | In short, SSE is a great tool for streaming quick real-time data. They offer unidirectional communication from a server to its clients and are typically used for updating dynamic content on web pages. 12 | 13 | We will be using python and FastAPI. FastAPI is a great tool for SSE applications as it is really easy to use and is built upon starlette which has SSE capabilities built in. 14 | 15 | When working with Server Sent Events, communications between client and server are initiated by the client (browser). The client creates a new JavaScript EventSource object, passing it the URL of an endpoint which is expected to return a stream of events over time. 16 | 17 | The server receives a regular HTTP request from the client (these should pass through firewalls etc like any other HTTP request which can make this method work in situations where WebSockets may be blocked). The client expects a response with a series of event messages at arbitrary times. The server needs to leave the HTTP response open until it has no more events to send, decides that the connection has been open long enough and can be considered stale, or until the client explicitly closes the initial request. 18 | 19 | Every time that the server writes an event to the HTTP response, the client will receive it and process it in a listener callback function. 20 | 21 | ## Client Side 22 | 23 | ```javascript 24 | const evtSource = new EventSource("http://127.0.0.1:8000/stream"); 25 | 26 | useEffect(() => { 27 | evtSource.addEventListener("new_message", function (event) { 28 | // Logic to handle status updates 29 | setMessage((messages) => [...messages, event.data]); 30 | }); 31 | 32 | evtSource.addEventListener("end_event", function (event) { 33 | setMessage((messages) => [...messages, event.data]); 34 | evtSource.close(); 35 | }); 36 | 37 | return () => { 38 | evtSource.close(); 39 | }; 40 | }, []); 41 | ``` 42 | 43 | ## Server side 44 | 45 | ```python 46 | from sse_starlette.sse import EventSourceResponse 47 | 48 | @app.get("/stream") 49 | async def message_stream(request: Request): 50 | async def event_generator(): 51 | while True: 52 | if await request.is_disconnected(): 53 | logger.debug("Request disconnected") 54 | break 55 | 56 | # Checks for new messages and return them to client if any 57 | counter, exists = get_message() 58 | if exists: 59 | yield { 60 | "event": "new_message", 61 | "id": "message_id", 62 | "retry": MESSAGE_STREAM_RETRY_TIMEOUT, 63 | "data": f"Counter value {counter}", 64 | } 65 | else: 66 | yield { 67 | "event": "end_event", 68 | "id": "message_id", 69 | "retry": MESSAGE_STREAM_RETRY_TIMEOUT, 70 | "data": "End of the stream", 71 | } 72 | 73 | await asyncio.sleep(MESSAGE_STREAM_DELAY) 74 | 75 | return EventSourceResponse(event_generator()) 76 | ``` 77 | 78 | Check out the code to get the working example. 79 | -------------------------------------------------------------------------------- /react-server-side-events/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-server-side-events", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "react": "^17.0.2", 10 | "react-dom": "^17.0.2", 11 | "react-scripts": "4.0.3", 12 | "web-vitals": "^1.0.1" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "eslintConfig": { 21 | "extends": [ 22 | "react-app", 23 | "react-app/jest" 24 | ] 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /react-server-side-events/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harshitsinghai77/server-sent-events-using-fastapi-and-reactjs/6bfebd7036e6f08b89afb7edccb5a3ac3c4c1d86/react-server-side-events/public/favicon.ico -------------------------------------------------------------------------------- /react-server-side-events/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /react-server-side-events/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harshitsinghai77/server-sent-events-using-fastapi-and-reactjs/6bfebd7036e6f08b89afb7edccb5a3ac3c4c1d86/react-server-side-events/public/logo192.png -------------------------------------------------------------------------------- /react-server-side-events/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harshitsinghai77/server-sent-events-using-fastapi-and-reactjs/6bfebd7036e6f08b89afb7edccb5a3ac3c4c1d86/react-server-side-events/public/logo512.png -------------------------------------------------------------------------------- /react-server-side-events/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /react-server-side-events/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /react-server-side-events/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /react-server-side-events/src/App.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import logo from "./logo.svg"; 3 | import "./App.css"; 4 | 5 | const evtSource = new EventSource("http://127.0.0.1:8000/stream"); 6 | 7 | function App() { 8 | const [messages, setMessage] = useState([]); 9 | useEffect(() => { 10 | evtSource.addEventListener("new_message", function (event) { 11 | // Logic to handle status updates 12 | setMessage((messages) => [...messages, event.data]); 13 | }); 14 | 15 | evtSource.addEventListener("end_event", function (event) { 16 | setMessage((messages) => [...messages, event.data]); 17 | evtSource.close(); 18 | }); 19 | 20 | return () => { 21 | evtSource.close(); 22 | }; 23 | }, []); 24 | return ( 25 |
26 |
27 | logo 28 | {messages.map((el) => ( 29 |

{el}

30 | ))} 31 |
32 |
33 | ); 34 | } 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /react-server-side-events/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /react-server-side-events/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /react-server-side-events/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /react-server-side-events/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /react-server-side-events/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /react-server-side-events/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | sse-starlette 3 | uvicorn[standard] 4 | asyncio --------------------------------------------------------------------------------