Repository: mfrachet/use-socketio
Branch: master
Commit: 9c3d1d11984a
Files: 55
Total size: 38.3 KB
Directory structure:
gitextract_ge2hlk0j/
├── .github/
│ └── workflows/
│ └── test_pr.yml
├── .gitignore
├── LICENSE.md
├── README.md
├── cypress/
│ ├── fixtures/
│ │ └── example.json
│ ├── integration/
│ │ ├── socketio.spec.js
│ │ ├── sse.spec.js
│ │ └── websocket.spec.js
│ ├── plugins/
│ │ └── index.js
│ └── support/
│ ├── commands.js
│ └── index.js
├── cypress.json
├── example/
│ ├── App.js
│ ├── index.html
│ ├── index.js
│ ├── server-helpers.js
│ ├── server.js
│ ├── socketio/
│ │ ├── SocketIo.js
│ │ ├── constants.js
│ │ └── socketio-server.js
│ ├── sse/
│ │ ├── SSE.js
│ │ ├── constants.js
│ │ └── sse-server.js
│ └── websocket/
│ ├── Websocket.js
│ ├── constants.js
│ └── websocket-server.js
├── lerna.json
├── package.json
└── packages/
├── use-server-sent-events/
│ ├── .npmignore
│ ├── README.md
│ ├── package.json
│ ├── src/
│ │ ├── context.ts
│ │ ├── hooks.ts
│ │ ├── index.tsx
│ │ └── provider.tsx
│ ├── tsconfig.json
│ └── tslint.json
├── use-socketio/
│ ├── .npmignore
│ ├── README.md
│ ├── package.json
│ ├── src/
│ │ ├── context.ts
│ │ ├── hooks.ts
│ │ ├── index.tsx
│ │ └── provider.tsx
│ ├── tsconfig.json
│ └── tslint.json
└── use-websockets/
├── .npmignore
├── README.md
├── package.json
├── src/
│ ├── context.ts
│ ├── hooks.ts
│ ├── index.tsx
│ └── provider.tsx
├── tsconfig.json
└── tslint.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/test_pr.yml
================================================
# This workflow will run when a pull request event is triggered (push, open, whatever happens)
name: Node.js test
on:
pull_request:
branches: ['main']
workflow_dispatch: # just in case when you need to trigger the thing manually without pull requests
jobs:
cypress-run:
runs-on: ubuntu-16.04
continue-on-error: true
steps:
- name: Install libgconf-2-4
run: sudo apt-get install libgconf-2-4 # Don't worry about passwords, sudo won't prompt for any, cuz there isn't a password here.
- uses: actions/checkout@v2
- name: Setup Node.js v10 🎁
uses: actions/setup-node@v1
with:
node-version: 10.x
- name: Install dependencies 📦
run: npm ci
- name: Setup cache
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Bootstrap 🥾
run: npm run bootstrap
- name: restore lerna
uses: actions/cache@v2
with:
path: |
node_modules
*/*/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Lint 🔍
run: npm run check:lint
- name: Build the hooks 🛠
run: npm run build
- name: Test the hooks ✔
uses: cypress-io/github-action@v2
with:
start: npm start, npm run start:test-server
- name: Upload the videos
uses: actions/upload-artifact@v2
with:
name: cypress-videos
path: cypress/videos/*
================================================
FILE: .gitignore
================================================
node_modules
.cache
dist
cypress/videos
lib
================================================
FILE: LICENSE.md
================================================
Copyright (c) 2004-Today Marvin Frachet
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
[](https://travis-ci.org/mfrachet/use-socketio)
React hooks for handling server-push technologies:
- [use-socketio](./packages/use-socketio) for [Socket.io](https://socket.io/)
- [use-server-sent-events](./packages/use-server-sent-events) for [Server Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)
- [use-websockets](./packages/use-websockets) for [Websocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)
## Running samples
To run samples locally, you can:
```sh
$ git clone https://github.com/mfrachet/server-push-hooks
$ cd server-push-hooks
$ npm install # install lerna and dependencies at the root
$ npm run bootstrap # install lerna packages dependencies
$ npm run build # build the lerna packages
$ npm start # start the web application
$ npm start:test-server # start the backend services in another terminal
$ npm run e2e # run E2E tests of the projects in another terminal
```
================================================
FILE: cypress/fixtures/example.json
================================================
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
================================================
FILE: cypress/integration/socketio.spec.js
================================================
context("Socket.io", () => {
beforeEach(() => {
cy.visit("http://localhost:1234/socket-io");
});
describe("Last message section", () => {
it("should be an empty section at the beginning", () => {
cy.get("[data-cy=last-message]").invoke("text").should("eq", "");
});
it("should display a message in the last message section", () => {
cy.get("[data-cy=add-one-last-message]").click();
cy.get("[data-cy=last-message]")
.invoke("text")
.should("eq", "This is one new message");
});
it("should display only the last message in the last message section while triggering three", () => {
cy.get("[data-cy=add-three-last-messages]").click();
cy.get("[data-cy=last-message]")
.invoke("text")
.should("eq", "This is a third new message");
});
it("should successfully manage subscription and unsubscription for a specific event", () => {
cy.get("[data-cy=add-one-last-message]").click();
cy.wait(100);
cy.get("[data-cy=unsubscribe-last-message]").click();
cy.get("[data-cy=add-three-last-messages]").click();
cy.get("[data-cy=last-message]")
.invoke("text")
.should("eq", "This is one new message");
cy.get("[data-cy=subscribe-last-message]").click();
cy.wait(100);
cy.get("[data-cy=add-three-last-messages]").click();
cy.get("[data-cy=last-message]")
.invoke("text")
.should("eq", "This is a third new message");
});
});
describe('All "message" event messages', () => {
beforeEach(() => {
cy.get("[data-cy=clear-messages]").click();
});
it("should be an empty section at the beginning", () => {
cy.get("[data-cy=messages]").invoke("text").should("eq", "");
});
it("should display 3 messages", () => {
cy.get("[data-cy=add-three-messages]").click();
cy.wait(100);
cy.get("[data-cy=messages]")
.children("li")
.eq(2)
.invoke("text")
.should("eq", "This is one new message");
cy.get("[data-cy=messages]")
.children("li")
.eq(1)
.invoke("text")
.should("eq", "This is a second new message");
cy.get("[data-cy=messages]")
.children("li")
.eq(0)
.invoke("text")
.should("eq", "This is a third new message");
});
it("should manage the subscription and unsubscription flows", () => {
cy.get("[data-cy=add-three-messages]").click();
cy.wait(100);
cy.get("[data-cy=messages]").children("li").its("length").should("eq", 3);
cy.get("[data-cy=unsubscribe-messages").click();
cy.get("[data-cy=add-three-messages]").click();
cy.wait(100);
cy.get("[data-cy=messages]").children("li").its("length").should("eq", 3);
cy.get("[data-cy=subscribe-messages").click();
cy.get("[data-cy=add-three-messages]").click();
cy.wait(100);
cy.get("[data-cy=messages]").children("li").its("length").should("eq", 6);
});
});
});
================================================
FILE: cypress/integration/sse.spec.js
================================================
context("Server sent event", () => {
describe("Last message for SSE", () => {
beforeEach(() => {
cy.visit("http://localhost:1234/last-sse");
});
it("should be an empty section at the beginning", () => {
cy.get("[data-cy=last-sse-message]").invoke("text").should("eq", "");
});
it("should have 'Marvin' as first SSE message", () => {
cy.get("[data-cy=last-sse-message]").should("contain", "Marvin");
});
it("should have 'Laetitia' as second SSE message", () => {
cy.get("[data-cy=last-sse-message]").should("contain", "Laetitia");
});
});
describe("All messages from SSE", () => {
beforeEach(() => {
cy.visit("http://localhost:1234/all-sse");
});
it("should display all the names", () => {
cy.get("[data-cy=all-sse-messages]").should("contain", "Marvin");
cy.get("[data-cy=all-sse-messages]").should("contain", "Laetitia");
});
});
});
================================================
FILE: cypress/integration/websocket.spec.js
================================================
context("Socket.io", () => {
beforeEach(() => {
cy.visit("http://localhost:1234/websocket");
cy.contains("Clear all messages").click();
});
describe("Connection", () => {
it("should open a websocket connection", () => {
cy.contains("Connection is opened.").should("be.visible");
});
});
describe("Last message section", () => {
beforeEach(() => {
cy.contains("Switch to Last message").click();
});
it("should be an empty section at the beginning", () => {
cy.contains("No last messages").should("be.visible");
});
it("should show one last message", () => {
cy.contains("Ask for one last message").click();
cy.contains("One last message").should("be.visible");
});
it("should show three last message", () => {
cy.contains("Ask for three last messages").click();
cy.contains("Three last message of three").should("be.visible");
});
});
describe("All message section", () => {
it("should be an empty section at the beginning", () => {
cy.contains("No messages").should("be.visible");
});
it("should show three messages", () => {
cy.contains("Ask for messages").click();
cy.contains("One of three messages").should("be.visible");
cy.contains("Two of three messages").should("be.visible");
cy.contains("Three of three messages").should("be.visible");
});
});
});
================================================
FILE: cypress/plugins/index.js
================================================
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}
================================================
FILE: cypress/support/commands.js
================================================
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
================================================
FILE: cypress/support/index.js
================================================
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
================================================
FILE: cypress.json
================================================
{
"defaultCommandTimeout": 10000
}
================================================
FILE: example/App.js
================================================
import React from "react";
import { SocketIoExample } from "./socketio/SocketIo";
import { AllSSEMessagesExample, LastSSEMessageExample } from "./sse/SSE";
import { WebsocketExample } from "./websocket/Websocket";
import { Link, Router } from "@reach/router";
export const App = () => (
);
================================================
FILE: example/index.html
================================================
React starter app
================================================
FILE: example/index.js
================================================
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";
const mountNode = document.getElementById("app");
ReactDOM.render(, mountNode);
================================================
FILE: example/server-helpers.js
================================================
const express = require("express");
const http = require("http");
const cors = require("cors");
const createHttpServer = () => {
const app = express();
const server = http.createServer(app);
app.use(cors());
return { server, app };
};
module.exports = createHttpServer;
================================================
FILE: example/server.js
================================================
const startSocketIoServer = require("./socketio/socketio-server");
const startSSEServer = require("./sse/sse-server");
const startWebsocketServer = require("./websocket/websocket-server");
startSocketIoServer();
startSSEServer();
startWebsocketServer();
================================================
FILE: example/socketio/SocketIo.js
================================================
import React, { useEffect, useState } from "react";
import {
SocketIOProvider,
useLastMessage,
useSocket,
} from "../../packages/use-socketio";
import { port } from "./constants";
const LastMessage = () => {
const [lastMessage, setLastMessage] = useState();
const { data, socket, unsubscribe, subscribe } = useLastMessage(
"last-messages"
);
// synchronize state with data so that we can clear it in the ui
useEffect(() => {
setLastMessage(data);
}, [data]);
return (
{tab === "all" && }
{tab !== "all" && }
);
};
================================================
FILE: example/websocket/constants.js
================================================
module.exports = {
port: 3124,
};
================================================
FILE: example/websocket/websocket-server.js
================================================
const WebSocket = require("ws");
const createHttpServer = require("../server-helpers");
const { port } = require("./constants");
const startWebsocketServer = () => {
const { server } = createHttpServer();
const wss = new WebSocket.Server({ server });
wss.on("connection", function connection(ws) {
const sendMessage = (type, message) => {
ws.send(JSON.stringify({ type, message }));
};
ws.on("message", function incoming(message) {
switch (message) {
case "one-last":
sendMessage("one-last", "One last message");
break;
case "three-last-messages":
sendMessage("three-last-messages", "One last message of three");
sendMessage("three-last-messages", "Two last message of three");
sendMessage("three-last-messages", "Three last message of three");
break;
case "all-messages":
sendMessage("all-messages", "One of three messages");
sendMessage("all-messages", "Two of three messages");
sendMessage("all-messages", "Three of three messages");
break;
}
});
ws.send("Opened from the server");
});
server.listen(port, () => {
console.log(`[Websocket] Started on port :${port}`);
});
};
module.exports = startWebsocketServer;
================================================
FILE: lerna.json
================================================
{
"packages": [
"packages/*"
],
"version": "2.1.1"
}
================================================
FILE: package.json
================================================
{
"name": "root",
"private": true,
"scripts": {
"bootstrap": "lerna bootstrap",
"e2e:ci": "cypress run",
"e2e": "cypress open",
"build": "lerna run build",
"format": "lerna run format",
"lint": "lerna run lint",
"check:lint": "lerna run check:lint",
"start": "parcel example/index.html",
"start:test-server": "node ./example/server",
"release": "npm i && npm run bootstrap && npm run check:lint && npm run build && lerna version && lerna publish"
},
"devDependencies": {
"@reach/router": "^1.3.4",
"cors": "^2.8.5",
"cypress": "^5.2.0",
"express": "^4.17.1",
"lerna": "^3.22.1",
"parcel-bundler": "^1.12.4",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"socket.io": "^3.0.3",
"ws": "^7.3.1"
}
}
================================================
FILE: packages/use-server-sent-events/.npmignore
================================================
example
cypress
src
node_modules
dist
.cache
================================================
FILE: packages/use-server-sent-events/README.md
================================================
Use [Server Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) with React, in hooks (React v16.8+).
# Usage
## Installation
```sh
$ yarn add use-server-sent-events
```
## In your code
### useSSE hook
Listen to a specific event and trigger the according callback every time there's one. **This hooks doesn't trigger a re-render. You have to manage it yourself.**
```jsx
import { SSEProvider, useSSE } from "use-server-sent-events";
const Parent = () => (
console.log("connection opened")}
>
);
const Children = () => {
const [messages, setMessages] = useState([]);
const error = useSSE((nextMessage) =>
setMessages([...messages, nextMessage])
);
return (
{messages.map((msg, index) => (
{msg}
))}
);
};
```
### useLastSSE hook
Listen to the latest message received on a specific event name. **This hook triggers a re-render so you don't have to.**
```jsx
import { SSEProvider, useLastSSE } from "use-server-sent-events";
const Parent = () => (
);
const Children = () => {
const { data, error } = useLastSSE();
return
{data || "No message yet"}
;
};
```
## Notes
For example on how to implement a Server Sent Event server, you can take a look at the [Server Sent Event example folder](../../example/sse/sse-server.js).
================================================
FILE: packages/use-server-sent-events/package.json
================================================
{
"name": "use-server-sent-events",
"version": "2.1.0",
"description": "React hooks for https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events",
"main": "./lib/index.js",
"author": "Marvin Frachet ",
"license": "MIT",
"private": false,
"homepage": "https://github.com/mfrachet/server-push-hooks",
"repository": {
"type": "git",
"url": "https://github.com/mfrachet/server-push-hooks"
},
"keywords": [
"react",
"context"
],
"scripts": {
"build": "yarn lint && tsc --outDir lib",
"format": "prettier --write './src/**/*.tsx'",
"lint": "tslint -c tslint.json 'src/**/**'",
"check:lint": "tsc --noEmit && yarn lint"
},
"devDependencies": {
"@babel/core": "^7.11.6",
"@babel/preset-env": "^7.11.5",
"@babel/preset-react": "^7.10.4",
"@types/react": "^16.9.49",
"@types/react-dom": "^16.9.8",
"prettier": "^2.1.2",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"tslint-plugin-prettier": "^2.3.0",
"typescript": "^4.0.3"
},
"peerDependencies": {
"react": "^16.13.1"
}
}
================================================
FILE: packages/use-server-sent-events/src/context.ts
================================================
import { createContext } from "react";
export const SSEContext = createContext(undefined);
================================================
FILE: packages/use-server-sent-events/src/hooks.ts
================================================
import { useContext, useEffect, useRef, useState } from "react";
import { SSEContext } from "./context";
export const useLastSSE = () => {
const [data, setData] = useState(undefined);
const [error, setError] = useState(undefined);
const eventSource = useContext(SSEContext);
useEffect(() => {
eventSource.onmessage = (e) => {
let message;
try {
message = JSON.parse(e.data);
} catch {
message = e.data;
}
setData(message);
};
eventSource.onerror = (e) => {
setError(e);
};
}, []);
return { data, error };
};
export const useSSE = (onMessage: (data: JSON) => void) => {
const [error, setError] = useState(undefined);
const onMessageRef = useRef(undefined);
onMessageRef.current = onMessage;
const eventSource = useContext(SSEContext);
useEffect(() => {
eventSource.onmessage = (e) => {
let message;
try {
message = JSON.parse(e.data);
} catch {
message = e.data;
}
onMessageRef.current(message);
};
eventSource.onerror = (e) => {
setError(e);
};
}, []);
return error;
};
================================================
FILE: packages/use-server-sent-events/src/index.tsx
================================================
export * from "./context";
export * from "./provider";
export * from "./hooks";
================================================
FILE: packages/use-server-sent-events/src/provider.tsx
================================================
import * as React from "react";
import { SSEContext } from "./context";
export interface ISSEProviderProps {
url: string;
opts?: EventSourceInit;
onOpen?: (ev: Event) => void;
}
export const SSEProvider: React.FC = ({
url,
opts,
children,
onOpen,
}) => {
const eventSourceRef = React.useRef();
const onOpenRef = React.useRef<(ev: Event) => void>();
if (!window) {
return <>{children}>;
}
if (!eventSourceRef.current) {
eventSourceRef.current = new EventSource(url, opts);
}
onOpenRef.current = onOpen;
React.useEffect(() => {
if (onOpenRef?.current) {
eventSourceRef.current.onopen = onOpenRef.current;
}
return () => {
eventSourceRef?.current?.close();
};
}, []);
return (
{children}
);
};
================================================
FILE: packages/use-server-sent-events/tsconfig.json
================================================
{
"compilerOptions": {
"declaration": true,
"esModuleInterop": true,
"jsx": "react",
"module": "commonjs",
"noImplicitAny": false,
"noImplicitReturns": true,
"skipLibCheck": true
},
"exclude": [],
"include": ["./src/**/*"]
}
================================================
FILE: packages/use-server-sent-events/tslint.json
================================================
{
"extends": [
"tslint:latest",
"tslint-plugin-prettier",
"tslint-config-prettier"
],
"jsRules": {},
"rules": {},
"rulesDirectory": []
}
================================================
FILE: packages/use-socketio/.npmignore
================================================
example
cypress
src
node_modules
dist
.cache
================================================
FILE: packages/use-socketio/README.md
================================================
Use [socket.io v3](https://socket.io/) with React, in hooks (React v16.8+).
_If you want to use socket.io in v2, you might want to use the v2.0.4 of this package. The last commit related to the v2 version is [this one](https://github.com/mfrachet/server-push-hooks/tree/4636e16f6753c5a49a52b0091ec92fce44e9913b)._
# Usage
## Installation
```sh
$ yarn add use-socketio
```
## In your code
### useSocket hook
Listen to a specific event and trigger the according callback every time there's one. **This hooks doesn't trigger a re-render. You have to manage it yourself.**
```jsx
import { SocketIOProvider, useSocket } from "use-socketio";
const Twitter = () => {
const [tweets, setTweet] = useState([]);
const { socket, subscribe, unsubscribe } = useSocket("tweet", (newTweet) =>
setTweet([newTweet, ...tweets])
);
return tweets.length ? (
{tweets.map((tweet) => (
{tweet.text}
))}
) : (
Actually waiting for the websocket server...
);
};
const App = () => (
);
```
_The socketio options to pass to the provider are available here: https://socket.io/docs/client-api/#new-Manager-url-options._
### useLastMessage hook
Listen to the latest message received on a specific event name. **This hook triggers a re-render so you don't have to.**
```jsx
import { SocketIOProvider, useLastMessage } from "use-socketio";
const Twitter = () => {
const { data: lastMessage, socket, subscribe, unsubscribe } = useLastMessage(
"tweet"
);
return
);
};
```
### useLastWebsocketMessage hook
Listen to the latest message received on a specific event name. **This hook triggers a re-render so you don't have to.**
```jsx
import { WebsocketProvider, useLastWebsocketMessage } from "use-websockets";
const Parent = () => (
);
const Children = () => {
const { data, error, ws } = useLastWebsocketMessage();
return