├── .gitignore ├── .npmrc ├── README.md ├── examples ├── next │ ├── .eslintrc.json │ ├── .gitignore │ ├── .test-dev.test.ts │ ├── .testRun.ts │ ├── .vscode │ │ └── settings.json │ ├── README.md │ ├── components │ │ └── socket.tsx │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ ├── api │ │ │ ├── hello.ts │ │ │ └── socket.ts │ │ └── index.tsx │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ ├── Home.module.css │ │ └── globals.css │ └── tsconfig.json └── rakkas │ ├── .eslintrc.cjs │ ├── .prettierrc │ ├── global.d.ts │ ├── package.json │ ├── src │ ├── common-hooks.tsx │ ├── entry-hattip.ts │ └── routes │ │ └── index.page.tsx │ ├── tsconfig.json │ └── vite.config.ts ├── package.json ├── packages └── socket.io-react-hook │ ├── .npmignore │ ├── README.md │ ├── package.json │ ├── src │ ├── IoContext.tsx │ ├── IoProvider.tsx │ ├── index.tsx │ ├── types.ts │ ├── useSocket.tsx │ ├── useSocketEvent.tsx │ └── utils │ │ ├── hash.ts │ │ └── url.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── test-e2e.config.mjs └── turbo.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .turbo 4 | .next 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies = false 2 | shamefully-hoist=true 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | React hooks for socket.io 4.x 2 | 3 | Examples: 4 | - [Next.js](examples/next/) 5 | - [Rakkas](examples/rakkas/) - [Try on StackBlitz](https://stackblitz.com/github/nitedani/socket.io-react-hook/tree/main/examples/rakkas?file=src%2Froutes%2Findex.page.tsx) 6 | 7 | --- 8 | Usage:
9 | 1. Wrap your components with the provider 10 | 11 | ```tsx 12 | import { IoProvider } from 'socket.io-react-hook'; 13 | 14 | 15 | 16 | 17 | ``` 18 | 19 | 2. 20 | ```tsx 21 | import { useSocket, useSocketEvent } from 'socket.io-react-hook'; 22 | 23 | const { socket, error } = useSocket(); 24 | const { lastMessage, sendMessage } = useSocketEvent(socket, 'message'); 25 | 26 | OR 27 | 28 | const { lastMessage, sendMessage } = useSocketEvent('message'); 29 | ``` 30 | 31 | If the socket parameter is not passed to useSocketEvent, the options of useSocket can be passed in the second parameter of useSocketEvent. 32 | For example 33 | ```tsx 34 | const { lastMessage, sendMessage } = useSocketEvent('message', { path: "/socket", extraHeaders: ... }); 35 | ``` 36 | 37 | useSocket and useSocketEvent forwards all relevant parameters to socket.io constructor.
38 | See the available options [here](https://socket.io/docs/v4/client-initialization/) 39 | 40 | If the socket connection depends on state, use it like this:
41 | The connection will be initiated once the socket is enabled.
42 | The connection for a namespace is shared between your components, feel free to use the hooks in multiple components. 43 | 44 | ```tsx 45 | import { useCookie } from 'react-use'; 46 | import { useSocket } from 'socket.io-react-hook'; 47 | 48 | export const useAuthenticatedSocket = (namespace?: string) => { 49 | const [accessToken] = useCookie('jwt'); 50 | return useSocket(namespace, { 51 | enabled: !!accessToken, 52 | }); 53 | }; 54 | 55 | ``` 56 | 57 | The useSocket hook always returns a socket-like object, even before a succesful connection. You don't have to check whether it is undefined.
58 | 59 | Example: 60 | 61 | ```tsx 62 | export const useAuthenticatedSocket = (namespace?: string) => { 63 | const [accessToken] = useCookie('jwt'); 64 | return useSocket(namespace, { 65 | enabled: !!accessToken, 66 | }); 67 | }; 68 | const Index = () => { 69 | 70 | const { socket, connected, error } = useAuthenticatedSocket(); 71 | const { lastMessage, sendMessage } = 72 | useSocketEvent(socket, 'eventName'); 73 | 74 | return
{ lastMessage }
75 | } 76 | ``` 77 | 78 | 79 | 80 | ```tsx 81 | const Index = () => { 82 | const [messages, setMessages] = useState([]); 83 | const { socket, connected, error } = useAuthenticatedSocket(); 84 | const onMessage = (message) => setMessages((state) => [...state, message]); 85 | useSocketEvent(socket, "eventName", { onMessage }); 86 | ... 87 | }; 88 | ``` 89 | 90 | useSocketEvent parameters: 91 | - socket: SocketIo object 92 | - event: string 93 | - options: 94 | - onMessage: (message) => void 95 | - keepPrevious: (default false) if true, useSocketEvent will immediately return the last available value of lastMessage after being remounted 96 | 97 | 98 | 99 | Emitting messages works as always: 100 | 101 | ```tsx 102 | const { socket, connected, error } = useSocket(); 103 | socket.emit('eventName', data); 104 | 105 | ``` 106 | Or by calling sendMessage 107 | ```tsx 108 | //Client 109 | const { socket, lastMessage, sendMessage } = useSocketEvent(socket, 'eventName'); 110 | ... 111 | const response = await sendMessage<{ status: string }>("hi server"); 112 | console.log(response.status) // "ok" 113 | 114 | //Server 115 | io.on("connection", (socket) => { 116 | socket.on("eventName", (message, callback) => { 117 | console.log(message) // "hi server" 118 | callback({ 119 | status: "ok" 120 | }); 121 | }); 122 | }); 123 | 124 | ``` 125 | 126 | [Typescript usage](https://socket.io/docs/v4/typescript/#types-for-the-client): 127 | 128 | ```ts 129 | interface ServerToClientEvents { 130 | noArg: () => void; 131 | basicEmit: (a: number, b: string, c: any) => void; 132 | withAck: (d: string, callback: (e: number) => void) => void; 133 | } 134 | 135 | interface ClientToServerEvents { 136 | hello: () => void; 137 | } 138 | const { socket } = useSocket(); 139 | 140 | socket.on("withAck", (d, callback) => {}); 141 | socket.emit("hello"); 142 | ``` -------------------------------------------------------------------------------- /examples/next/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/babel","next/core-web-vitals"] 3 | } -------------------------------------------------------------------------------- /examples/next/.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 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /examples/next/.test-dev.test.ts: -------------------------------------------------------------------------------- 1 | import { testRun } from "./.testRun"; 2 | 3 | testRun({ cmd: "npm run dev" }); 4 | -------------------------------------------------------------------------------- /examples/next/.testRun.ts: -------------------------------------------------------------------------------- 1 | import { page, run, test, urlBase } from "@brillout/test-e2e"; 2 | import killPort from "kill-port"; 3 | export { testRun }; 4 | 5 | function testRun({ 6 | cmd, 7 | port, 8 | }: { 9 | cmd: "npm run dev" | "npm run preview"; 10 | port?: number; 11 | }) { 12 | killPort(3000); 13 | run(cmd, { serverIsReadyMessage: "ready" }); 14 | 15 | test("logs Hello World!", async () => 16 | new Promise(async (resolve) => { 17 | page.on("console", (msg) => { 18 | if (msg.text().includes("Hello World!")) { 19 | resolve(); 20 | } 21 | }); 22 | 23 | await page.goto(urlBase); 24 | })); 25 | } 26 | -------------------------------------------------------------------------------- /examples/next/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": ".\\node_modules\\typescript\\lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } -------------------------------------------------------------------------------- /examples/next/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /examples/next/components/socket.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useSocket, useSocketEvent } from "socket.io-react-hook"; 3 | 4 | const Socket1 = () => { 5 | const [enabled, setEnabled] = useState(true); 6 | const { socket } = useSocket({ path: "/api/socket", enabled }); 7 | const { lastMessage } = useSocketEvent(socket, "message", { 8 | onMessage: (message) => console.log(message), 9 | }); 10 | return ( 11 |
18 |

Socket1

19 |
Message: {lastMessage}
20 | 21 |
22 | ); 23 | }; 24 | 25 | const Socket2 = () => { 26 | const [enabled, setEnabled] = useState(false); 27 | const { socket } = useSocket({ 28 | enabled, 29 | path: "/api/socket", 30 | extraHeaders: { Authorization: "Bearer xxx" }, 31 | }); 32 | const { lastMessage } = useSocketEvent(socket, "message", { 33 | onMessage: (message) => console.log(message), 34 | }); 35 | return ( 36 |
43 |

Socket2

44 |
Message: {lastMessage}
45 | 46 |
47 | ); 48 | }; 49 | 50 | export default function Socket() { 51 | const [show, setShow] = useState(false); 52 | 53 | return ( 54 |
55 |

Socket.io React Hook

56 | 57 | 60 | {show && } 61 | 62 | 65 | {show && } 66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /examples/next/next.config.js: -------------------------------------------------------------------------------- 1 | const { IgnorePlugin } = require("webpack"); 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | reactStrictMode: true, 6 | swcMinify: true, 7 | webpack: (config, options) => { 8 | if (options.isServer) { 9 | config.plugins.push( 10 | new IgnorePlugin({ 11 | resourceRegExp: /^(bufferutil|utf-8-validate)$/, 12 | }) 13 | ); 14 | } 15 | return config; 16 | }, 17 | }; 18 | 19 | module.exports = nextConfig; 20 | -------------------------------------------------------------------------------- /examples/next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-next", 3 | "version": "2.4.5", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "test": "" 11 | }, 12 | "dependencies": { 13 | "@types/node": "18.11.9", 14 | "@types/react": "^18.3.3", 15 | "@types/react-dom": "18.0.8", 16 | "eslint": "8.26.0", 17 | "eslint-config-next": "13.0.1", 18 | "next": "13.0.1", 19 | "react": "18.2.0", 20 | "react-dom": "18.2.0", 21 | "socket.io": "^4.7.5", 22 | "socket.io-react-hook": "^2.4.4", 23 | "typescript": "^4.9.5" 24 | }, 25 | "devDependencies": { 26 | "webpack": "^5.93.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/next/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from "next/app"; 2 | import { IoProvider } from "socket.io-react-hook"; 3 | 4 | export default function MyApp({ Component, pageProps }: AppProps) { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /examples/next/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /examples/next/pages/api/socket.ts: -------------------------------------------------------------------------------- 1 | import { Server } from "socket.io"; 2 | 3 | const SocketHandler = (req: any, res: any) => { 4 | if (!res.socket.server.io) { 5 | const io = new Server(res.socket.server, { path: "/api/socket" }); 6 | res.socket.server.io = io; 7 | io.on("connection", (socket) => { 8 | const interval = setInterval(() => { 9 | socket.emit("message", "Hello World!" + Date.now()); 10 | }, 1000); 11 | socket.on("disconnect", () => { 12 | clearInterval(interval); 13 | }); 14 | }); 15 | } 16 | res.end(); 17 | }; 18 | 19 | export default SocketHandler; 20 | -------------------------------------------------------------------------------- /examples/next/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Socket from "../components/socket"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /examples/next/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitedani/socket.io-react-hook/aafa7bfe51b33ecb479881434a56023190722ae8/examples/next/public/favicon.ico -------------------------------------------------------------------------------- /examples/next/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/next/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | 118 | @media (prefers-color-scheme: dark) { 119 | .card, 120 | .footer { 121 | border-color: #222; 122 | } 123 | .code { 124 | background: #111; 125 | } 126 | .logo img { 127 | filter: invert(1); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /examples/next/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/next/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/types/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /examples/rakkas/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | require("@rakkasjs/eslint-config/patch"); 2 | 3 | module.exports = { 4 | root: true, 5 | extends: ["@rakkasjs"], 6 | parserOptions: { tsconfigRootDir: __dirname }, 7 | settings: { 8 | "import/resolver": { 9 | typescript: { 10 | project: [__dirname + "/tsconfig.json"], 11 | }, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /examples/rakkas/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /examples/rakkas/global.d.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse, Server } from "http"; 2 | import type { Server as SocketIOServer } from "socket.io"; 3 | declare module "rakkasjs" { 4 | interface ServerSideLocals { 5 | io: SocketIOServer; 6 | } 7 | interface RequestContext { 8 | platform: { 9 | request: IncomingMessage & { 10 | socket: { 11 | server: Server & { 12 | io?: SocketIOServer; 13 | }; 14 | }; 15 | }; 16 | response: ServerResponse; 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/rakkas/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-rakkas", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "rakkas", 7 | "build": "rakkas build", 8 | "start": "node dist/server", 9 | "format": "prettier --write --ignore-unknown src", 10 | "test": "pnpm test:typecheck && pnpm test:format && pnpm test:lint", 11 | "test:typecheck": "tsc -p tsconfig.json --noEmit", 12 | "test:format": "prettier --check --ignore-unknown src", 13 | "test:lint": "eslint . --ignore-pattern dist" 14 | }, 15 | "devDependencies": { 16 | "@rakkasjs/eslint-config": "0.6.19", 17 | "@types/react": "^18.3.3", 18 | "@types/react-dom": "^18.3.0", 19 | "eslint": "^8.57.0", 20 | "prettier": "^2.8.8", 21 | "rakkasjs": "0.6.19", 22 | "typescript": "^4.9.5", 23 | "vite": "^4.5.3", 24 | "vite-tsconfig-paths": "^4.3.2" 25 | }, 26 | "dependencies": { 27 | "react": "^18.3.1", 28 | "react-dom": "^18.3.1", 29 | "socket.io": "^4.7.5", 30 | "socket.io-react-hook": "^2.4.4" 31 | }, 32 | "version": "2.4.5" 33 | } 34 | -------------------------------------------------------------------------------- /examples/rakkas/src/common-hooks.tsx: -------------------------------------------------------------------------------- 1 | import { IoProvider } from "socket.io-react-hook"; 2 | import type { CommonHooks } from "rakkasjs"; 3 | 4 | const hooks: CommonHooks = { 5 | wrapApp(app) { 6 | return {app}; 7 | }, 8 | }; 9 | 10 | export default hooks; 11 | -------------------------------------------------------------------------------- /examples/rakkas/src/entry-hattip.ts: -------------------------------------------------------------------------------- 1 | import { createRequestHandler, RequestContext } from "rakkasjs"; 2 | import { Server } from "socket.io"; 3 | const socketIoMiddleware = (ctx: RequestContext) => { 4 | const server = ctx.platform.request.socket.server; 5 | if (!server.io) { 6 | server.io = new Server(server); 7 | } 8 | ctx.locals.io = server.io; 9 | }; 10 | 11 | export default createRequestHandler({ 12 | middleware: { 13 | beforePages: [socketIoMiddleware], 14 | beforeApiRoutes: [socketIoMiddleware], 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /examples/rakkas/src/routes/index.page.tsx: -------------------------------------------------------------------------------- 1 | import { runSSM } from "rakkasjs"; 2 | import { useRef, useState } from "react"; 3 | import { useSocketEvent } from "socket.io-react-hook"; 4 | 5 | export default function HomePage() { 6 | const ref = useRef(null); 7 | const [messages, setMessages] = useState([]); 8 | 9 | useSocketEvent("message", { 10 | onMessage: (message) => setMessages((messages) => [...messages, message]), 11 | }); 12 | 13 | const sendMessage = (message: string) => 14 | runSSM((ctx) => ctx.locals.io.emit("message", message)); 15 | 16 | return ( 17 |
18 | 21 | 22 | {messages.map((message, i) => ( 23 |
{message}
24 | ))} 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /examples/rakkas/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "ESNext", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "Node", 10 | "resolveJsonModule": true, 11 | "jsx": "react-jsx", 12 | "baseUrl": ".", 13 | "types": ["vite/client"] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/rakkas/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import rakkas from "rakkasjs/vite-plugin"; 3 | import tsconfigPaths from "vite-tsconfig-paths"; 4 | 5 | export default defineConfig({ 6 | plugins: [tsconfigPaths(), rakkas()], 7 | }); 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socket.io-react-hook", 3 | "version": "2.4.5", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "turbo run build", 8 | "release": "bumpp package.json packages/*/package.json examples/*/package.json && pnpm -r publish --access=public", 9 | "test": "test-e2e" 10 | }, 11 | "devDependencies": { 12 | "@brillout/test-e2e": "^0.1.22", 13 | "bumpp": "^8.2.1", 14 | "turbo": "^1.13.4" 15 | }, 16 | "engines": {}, 17 | "dependencies": { 18 | "kill-port": "^2.0.1" 19 | }, 20 | "packageManager": "pnpm@9.6.0", 21 | "pnpm": { 22 | "overrides": { 23 | "socket.io-react-hook": "link:./packages/socket.io-react-hook/" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/socket.io-react-hook/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src 3 | !dist 4 | .turbo -------------------------------------------------------------------------------- /packages/socket.io-react-hook/README.md: -------------------------------------------------------------------------------- 1 | React hooks for socket.io 4.x 2 | 3 | Examples: 4 | - [Next.js](examples/next/) 5 | - [Rakkas](examples/rakkas/) - [Try on StackBlitz](https://stackblitz.com/github/nitedani/socket.io-react-hook/tree/main/examples/rakkas?file=src%2Froutes%2Findex.page.tsx) 6 | 7 | --- 8 | Usage:
9 | 1. Wrap your components with the provider 10 | 11 | ```tsx 12 | import { IoProvider } from 'socket.io-react-hook'; 13 | 14 | 15 | 16 | 17 | ``` 18 | 19 | 2. 20 | ```tsx 21 | import { useSocket, useSocketEvent } from 'socket.io-react-hook'; 22 | 23 | const { socket, error } = useSocket(); 24 | const { lastMessage, sendMessage } = useSocketEvent(socket, 'message'); 25 | 26 | OR 27 | 28 | const { lastMessage, sendMessage } = useSocketEvent('message'); 29 | ``` 30 | 31 | If the socket parameter is not passed to useSocketEvent, the options of useSocket can be passed in the second parameter of useSocketEvent. 32 | For example 33 | ```tsx 34 | const { lastMessage, sendMessage } = useSocketEvent('message', { path: "/socket", extraHeaders: ... }); 35 | ``` 36 | 37 | useSocket and useSocketEvent forwards all relevant parameters to socket.io constructor.
38 | See the available options [here](https://socket.io/docs/v4/client-initialization/) 39 | 40 | If the socket connection depends on state, use it like this:
41 | The connection will be initiated once the socket is enabled.
42 | The connection for a namespace is shared between your components, feel free to use the hooks in multiple components. 43 | 44 | ```tsx 45 | import { useCookie } from 'react-use'; 46 | import { useSocket } from 'socket.io-react-hook'; 47 | 48 | export const useAuthenticatedSocket = (namespace?: string) => { 49 | const [accessToken] = useCookie('jwt'); 50 | return useSocket(namespace, { 51 | enabled: !!accessToken, 52 | }); 53 | }; 54 | 55 | ``` 56 | 57 | The useSocket hook always returns a socket-like object, even before a succesful connection. You don't have to check whether it is undefined.
58 | 59 | Example: 60 | 61 | ```tsx 62 | export const useAuthenticatedSocket = (namespace?: string) => { 63 | const [accessToken] = useCookie('jwt'); 64 | return useSocket(namespace, { 65 | enabled: !!accessToken, 66 | }); 67 | }; 68 | const Index = () => { 69 | 70 | const { socket, connected, error } = useAuthenticatedSocket(); 71 | const { lastMessage, sendMessage } = 72 | useSocketEvent(socket, 'eventName'); 73 | 74 | return
{ lastMessage }
75 | } 76 | ``` 77 | 78 | 79 | 80 | ```tsx 81 | const Index = () => { 82 | const [messages, setMessages] = useState([]); 83 | const { socket, connected, error } = useAuthenticatedSocket(); 84 | const onMessage = (message) => setMessages((state) => [...state, message]); 85 | useSocketEvent(socket, "eventName", { onMessage }); 86 | ... 87 | }; 88 | ``` 89 | 90 | useSocketEvent parameters: 91 | - socket: SocketIo object 92 | - event: string 93 | - options: 94 | - onMessage: (message) => void 95 | - keepPrevious: (default false) if true, useSocketEvent will immediately return the last available value of lastMessage after being remounted 96 | 97 | 98 | 99 | Emitting messages works as always: 100 | 101 | ```tsx 102 | const { socket, connected, error } = useSocket(); 103 | socket.emit('eventName', data); 104 | 105 | ``` 106 | Or by calling sendMessage 107 | ```tsx 108 | //Client 109 | const { socket, lastMessage, sendMessage } = useSocketEvent(socket, 'eventName'); 110 | ... 111 | const response = await sendMessage<{ status: string }>("hi server"); 112 | console.log(response.status) // "ok" 113 | 114 | //Server 115 | io.on("connection", (socket) => { 116 | socket.on("eventName", (message, callback) => { 117 | console.log(message) // "hi server" 118 | callback({ 119 | status: "ok" 120 | }); 121 | }); 122 | }); 123 | 124 | ``` 125 | 126 | [Typescript usage](https://socket.io/docs/v4/typescript/#types-for-the-client): 127 | 128 | ```ts 129 | interface ServerToClientEvents { 130 | noArg: () => void; 131 | basicEmit: (a: number, b: string, c: any) => void; 132 | withAck: (d: string, callback: (e: number) => void) => void; 133 | } 134 | 135 | interface ClientToServerEvents { 136 | hello: () => void; 137 | } 138 | const { socket } = useSocket(); 139 | 140 | socket.on("withAck", (d, callback) => {}); 141 | socket.emit("hello"); 142 | ``` -------------------------------------------------------------------------------- /packages/socket.io-react-hook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socket.io-react-hook", 3 | "version": "2.4.5", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "author": { 9 | "email": "nitedani@gmail.com", 10 | "name": "Horváth Dániel" 11 | }, 12 | "keywords": [ 13 | "socket.io", 14 | "socket", 15 | "react", 16 | "hooks" 17 | ], 18 | "license": "ISC", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/nitedani/socket.io-react-hooks.git", 22 | "directory": "packages/socket.io-react-hook" 23 | }, 24 | "exports": { 25 | ".": { 26 | "types": "./dist/index.d.ts", 27 | "import": "./dist/index.mjs", 28 | "require": "./dist/index.js" 29 | }, 30 | "./package.json": "./package.json" 31 | }, 32 | "scripts": { 33 | "build": "tsup src --dts", 34 | "dev": "tsup src --dts --watch", 35 | "clean": "rimraf dist .turbo", 36 | "typecheck": "tsc --noEmit" 37 | }, 38 | "devDependencies": { 39 | "@socket.io/component-emitter": "^3.1.2", 40 | "@types/react": "^18.3.3", 41 | "rimraf": "^4.4.1", 42 | "tsup": "^6.7.0", 43 | "typescript": "^4.9.5" 44 | }, 45 | "dependencies": { 46 | "parseuri": "0.0.6", 47 | "socket.io-client": "^4.7.5", 48 | "socket.io-mock": "^1.3.2", 49 | "stable-hash": "^0.0.3" 50 | }, 51 | "peerDependencies": { 52 | "react": "*" 53 | }, 54 | "tsup": { 55 | "clean": true, 56 | "target": "es2019", 57 | "format": [ 58 | "cjs", 59 | "esm" 60 | ] 61 | }, 62 | "files": [ 63 | "/dist" 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /packages/socket.io-react-hook/src/IoContext.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { IoContextInterface } from "./types"; 4 | 5 | const IoContext = React.createContext>({ 6 | createConnection: () => undefined, 7 | getConnection: () => undefined, 8 | registerSharedListener: () => () => {}, 9 | }); 10 | 11 | export default IoContext; 12 | -------------------------------------------------------------------------------- /packages/socket.io-react-hook/src/IoProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | import io from "socket.io-client"; 3 | import IoContext from "./IoContext"; 4 | 5 | import { 6 | CreateConnectionFunc, 7 | IoConnection, 8 | IoNamespace, 9 | GetConnectionFunc, 10 | SocketLike, 11 | SocketState, 12 | } from "./types"; 13 | 14 | const IoProvider = function ({ children }: React.PropsWithChildren<{}>) { 15 | const connections = useRef>({}); 16 | const eventSubscriptions = useRef>({}); 17 | const sockets = useRef< 18 | Record< 19 | IoNamespace, 20 | { 21 | socket: IoConnection; 22 | } & SocketState 23 | > 24 | >({}); 25 | 26 | const createConnection: CreateConnectionFunc = ( 27 | namespaceKey, 28 | urlConfig, 29 | options = {} 30 | ) => { 31 | if (!(namespaceKey in connections.current)) { 32 | connections.current[namespaceKey] = 1; 33 | } else { 34 | connections.current[namespaceKey] += 1; 35 | } 36 | 37 | const cleanup = () => { 38 | if (--connections.current[namespaceKey] === 0) { 39 | const socketsToClose = Object.keys(sockets.current).filter((key) => 40 | key.includes(namespaceKey) 41 | ); 42 | 43 | for (const key of socketsToClose) { 44 | sockets.current[key].socket.disconnect(); 45 | sockets.current[key].subscribers.clear(); 46 | delete sockets.current[key]; 47 | } 48 | } 49 | }; 50 | 51 | // By default socket.io-client creates a new connection for the same namespace 52 | // The next line prevents that 53 | if (sockets.current[namespaceKey]) { 54 | sockets.current[namespaceKey].socket.connect(); 55 | return { 56 | cleanup, 57 | ...sockets.current[namespaceKey], 58 | }; 59 | } 60 | 61 | const handleConnect = () => { 62 | sockets.current[namespaceKey].state.status = "connected"; 63 | sockets.current[namespaceKey].notify("connected"); 64 | }; 65 | 66 | const handleDisconnect = () => { 67 | sockets.current[namespaceKey].state.status = "disconnected"; 68 | sockets.current[namespaceKey].notify("disconnected"); 69 | }; 70 | 71 | const socket = io(urlConfig.source, options) as SocketLike; 72 | socket.namespaceKey = namespaceKey; 73 | 74 | sockets.current = Object.assign({}, sockets.current, { 75 | [namespaceKey]: { 76 | socket, 77 | state: { 78 | status: "disconnected", 79 | lastMessage: {}, 80 | error: null, 81 | }, 82 | notify: (event: string) => { 83 | sockets.current[namespaceKey].subscribers.forEach((callback) => 84 | callback(sockets.current[namespaceKey].state, event) 85 | ); 86 | }, 87 | subscribers: new Set(), 88 | subscribe: (callback) => { 89 | sockets.current[namespaceKey].subscribers.add(callback); 90 | return () => 91 | sockets.current[namespaceKey]?.subscribers.delete(callback); 92 | }, 93 | }, 94 | }); 95 | 96 | const handleError = (error) => { 97 | sockets.current[namespaceKey].state.error = error; 98 | sockets.current[namespaceKey].notify("error"); 99 | }; 100 | socket.on("error", handleError); 101 | socket.on("connect_error", handleError); 102 | 103 | socket.on("connect", handleConnect); 104 | socket.on("disconnect", handleDisconnect); 105 | 106 | return { 107 | cleanup, 108 | ...sockets.current[namespaceKey], 109 | }; 110 | }; 111 | 112 | const getConnection: GetConnectionFunc = (namespaceKey = "") => 113 | sockets.current[namespaceKey]; 114 | 115 | const registerSharedListener = (namespaceKey = "", forEvent = "") => { 116 | if ( 117 | sockets.current[namespaceKey] && 118 | !sockets.current[namespaceKey].socket.hasListeners(forEvent) 119 | ) { 120 | sockets.current[namespaceKey].socket.on(forEvent, (message) => { 121 | sockets.current[namespaceKey].state.lastMessage[forEvent] = message; 122 | sockets.current[namespaceKey].notify("message"); 123 | }); 124 | } 125 | const subscriptionKey = `${namespaceKey}${forEvent}`; 126 | const cleanup = () => { 127 | if (--eventSubscriptions.current[subscriptionKey] === 0) { 128 | delete eventSubscriptions.current[subscriptionKey]; 129 | if (sockets.current[namespaceKey]) 130 | delete sockets.current[namespaceKey].state.lastMessage[forEvent]; 131 | } 132 | }; 133 | 134 | if (!(subscriptionKey in eventSubscriptions.current)) { 135 | eventSubscriptions.current[subscriptionKey] = 1; 136 | } else { 137 | eventSubscriptions.current[subscriptionKey] += 1; 138 | } 139 | 140 | return () => cleanup(); 141 | }; 142 | 143 | return ( 144 | 151 | {children} 152 | 153 | ); 154 | }; 155 | 156 | export default IoProvider; 157 | -------------------------------------------------------------------------------- /packages/socket.io-react-hook/src/index.tsx: -------------------------------------------------------------------------------- 1 | import IoProvider from "./IoProvider"; 2 | import IoContext from "./IoContext"; 3 | import useSocket from "./useSocket"; 4 | import useSocketEvent from "./useSocketEvent"; 5 | 6 | export { IoProvider, IoContext, useSocket, useSocketEvent }; 7 | -------------------------------------------------------------------------------- /packages/socket.io-react-hook/src/types.ts: -------------------------------------------------------------------------------- 1 | import { ManagerOptions, Socket, SocketOptions } from "socket.io-client"; 2 | import { url } from "./utils/url"; 3 | export type IoNamespace = string; 4 | 5 | export type IoConnection = Socket; 6 | 7 | export type SocketLike = T & { 8 | namespaceKey: string; 9 | }; 10 | 11 | export type SocketState = { 12 | state: { 13 | status: "disconnected" | "connecting" | "connected"; 14 | error: Error | null; 15 | lastMessage: Record; 16 | }; 17 | notify: (event: string) => void; 18 | subscribe: ( 19 | callback: (state: SocketState["state"], event: string) => void 20 | ) => () => void; 21 | subscribers: Set<(state: SocketState["state"], event: string) => void>; 22 | }; 23 | 24 | export type CleanupFunction = () => void; 25 | 26 | export type CreateConnectionFuncReturnType = { 27 | socket: SocketLike; 28 | cleanup: CleanupFunction; 29 | } & SocketState; 30 | 31 | export type CreateConnectionFunc = ( 32 | namespaceKey: string, 33 | urlConfig: ReturnType, 34 | options?: Partial | undefined 35 | ) => CreateConnectionFuncReturnType | undefined; 36 | 37 | export type GetConnectionFunc = (namespace?: IoNamespace) => 38 | | ({ 39 | socket: T; 40 | } & SocketState) 41 | | undefined; 42 | 43 | export type IoContextInterface = { 44 | createConnection: CreateConnectionFunc; 45 | getConnection: GetConnectionFunc; 46 | registerSharedListener: ( 47 | namespace: string, 48 | forEvent: string 49 | ) => CleanupFunction; 50 | }; 51 | 52 | export type UseSocketOptions = Partial & { 53 | enabled?: boolean; 54 | }; 55 | 56 | export type UseSocketReturnType = { 57 | socket: SocketLike; 58 | connected: boolean; 59 | error: any; 60 | }; 61 | export type UseSocketEventOptions = { 62 | keepPrevious?: boolean; 63 | onMessage?: (message: T) => void; 64 | }; 65 | export type UseSocketEventReturnType< 66 | T, 67 | EmitMessageArgs extends any[] = any[], 68 | R = any 69 | > = { 70 | sendMessage: (...message: EmitMessageArgs) => Promise; 71 | socket: SocketLike; 72 | status: "connecting" | "connected" | "disconnected"; 73 | error: Error | null; 74 | lastMessage: T; 75 | }; 76 | -------------------------------------------------------------------------------- /packages/socket.io-react-hook/src/useSocket.tsx: -------------------------------------------------------------------------------- 1 | import type { EventsMap, DefaultEventsMap } from "@socket.io/component-emitter"; 2 | import { Socket } from "socket.io-client"; 3 | import { url } from "./utils/url"; 4 | import { unique } from "./utils/hash"; 5 | import IoContext from "./IoContext"; 6 | import { 7 | IoContextInterface, 8 | IoNamespace, 9 | SocketLike, 10 | UseSocketOptions, 11 | UseSocketReturnType, 12 | } from "./types"; 13 | import SocketMock from "socket.io-mock"; 14 | import { useContext, useEffect, useRef, useState } from "react"; 15 | import stableHash from "stable-hash"; 16 | 17 | function useSocket< 18 | ListenEvents extends EventsMap = DefaultEventsMap, 19 | EmitEvents extends EventsMap = ListenEvents, 20 | SocketType extends Socket = Socket< 21 | ListenEvents, 22 | EmitEvents 23 | > 24 | >(options?: UseSocketOptions): UseSocketReturnType; 25 | function useSocket< 26 | ListenEvents extends EventsMap = DefaultEventsMap, 27 | EmitEvents extends EventsMap = ListenEvents, 28 | SocketType extends Socket = Socket< 29 | ListenEvents, 30 | EmitEvents 31 | > 32 | >( 33 | namespace: IoNamespace, 34 | options?: UseSocketOptions 35 | ): UseSocketReturnType; 36 | function useSocket< 37 | ListenEvents extends EventsMap = DefaultEventsMap, 38 | EmitEvents extends EventsMap = ListenEvents, 39 | SocketType extends Socket = Socket< 40 | ListenEvents, 41 | EmitEvents 42 | > 43 | >( 44 | namespace?: string | UseSocketOptions, 45 | options?: UseSocketOptions 46 | ): UseSocketReturnType { 47 | const isServer = typeof window === "undefined"; 48 | if (isServer) { 49 | return { 50 | socket: new SocketMock(), 51 | connected: false, 52 | error: null, 53 | }; 54 | } 55 | 56 | const opts = { 57 | namespace: typeof namespace === "string" ? namespace : "", 58 | options: typeof namespace === "object" ? namespace : options, 59 | }; 60 | 61 | const urlConfig = url( 62 | opts.namespace, 63 | opts.options?.path || "/socket.io", 64 | opts.options?.port 65 | ); 66 | const connectionKey = urlConfig.id; 67 | const hash = opts.options 68 | ? unique( 69 | stableHash( 70 | Object.entries(opts.options).reduce((acc, [k, v]) => { 71 | if (typeof v === "function") { 72 | return acc; 73 | } 74 | acc[k] = v; 75 | return acc; 76 | }, {}) 77 | ) 78 | ) 79 | : ""; 80 | const namespaceKey = `${connectionKey}${urlConfig.path}${hash}`; 81 | const enabled = opts.options?.enabled === undefined || opts.options.enabled; 82 | const { createConnection, getConnection } = 83 | useContext>(IoContext); 84 | 85 | const connection = getConnection(namespaceKey); 86 | 87 | const state = useRef<{ 88 | socket: SocketLike; 89 | status: "connecting" | "connected" | "disconnected"; 90 | error: Error | null; 91 | }>({ 92 | socket: connection?.socket || new SocketMock(), 93 | status: connection?.state.status || "disconnected", 94 | error: null, 95 | }); 96 | 97 | const [, rerender] = useState({}); 98 | const connected = state.current.status === "connected"; 99 | 100 | useEffect(() => { 101 | if (enabled) { 102 | const { 103 | socket: _socket, 104 | cleanup, 105 | subscribe, 106 | } = createConnection(namespaceKey, urlConfig, opts.options)!; 107 | state.current.socket = _socket; 108 | 109 | const unsubscribe = subscribe((newState) => { 110 | let changed = false; 111 | if (state.current.status !== newState.status) { 112 | state.current.status = newState.status; 113 | changed = true; 114 | } 115 | if (state.current.error !== newState.error) { 116 | state.current.error = newState.error; 117 | changed = true; 118 | } 119 | if (changed) { 120 | rerender({}); 121 | } 122 | }); 123 | 124 | rerender({}); 125 | 126 | return () => { 127 | unsubscribe(); 128 | cleanup(); 129 | }; 130 | } 131 | return () => {}; 132 | }, [enabled, namespaceKey]); 133 | 134 | return { 135 | socket: state.current.socket, 136 | error: state.current.error, 137 | connected, 138 | }; 139 | } 140 | 141 | export default useSocket; 142 | -------------------------------------------------------------------------------- /packages/socket.io-react-hook/src/useSocketEvent.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useRef, useState } from "react"; 2 | import SocketMock from "socket.io-mock"; 3 | import IoContext from "./IoContext"; 4 | import { 5 | IoContextInterface, 6 | SocketLike, 7 | UseSocketEventOptions, 8 | UseSocketEventReturnType, 9 | UseSocketOptions, 10 | } from "./types"; 11 | import useSocket from "./useSocket"; 12 | import type { Socket } from "socket.io-client"; 13 | import type { 14 | DefaultEventsMap, 15 | EventNames, 16 | EventsMap, 17 | } from "@socket.io/component-emitter"; 18 | 19 | // TODO: spread args in IoProvider and replace this with Parameters 20 | type Parameter any> = T extends ( 21 | ...args: infer P 22 | ) => any 23 | ? P[0] 24 | : never; 25 | 26 | // TODO: change any to unknown in major version 27 | function useSocketEvent( 28 | event: string, 29 | options?: UseSocketEventOptions & UseSocketOptions 30 | ): UseSocketEventReturnType; 31 | function useSocketEvent< 32 | T = never, 33 | ListenEvents extends EventsMap = DefaultEventsMap, 34 | EmitEvents extends EventsMap = ListenEvents, 35 | EventKey extends EventNames = EventNames, 36 | ListenMessageType = [T] extends [never] 37 | ? Parameter 38 | : T, 39 | EmitMessageArgs extends any[] = Parameters< 40 | EmitEvents[EventNames] 41 | >, 42 | //TODO: infer from last argument returntype(cb) of EmitEvents[EventKey] 43 | // if last argument is a function then infer return type 44 | // if last argument is not a function then infer void 45 | EmitMessageCbReturnType = any 46 | >( 47 | socket: SocketLike>, 48 | event: EventKey, 49 | options?: UseSocketEventOptions 50 | ): UseSocketEventReturnType< 51 | ListenMessageType, 52 | EmitMessageArgs, 53 | EmitMessageCbReturnType 54 | >; 55 | function useSocketEvent< 56 | T = never, 57 | ListenEvents extends EventsMap = DefaultEventsMap, 58 | EmitEvents extends EventsMap = ListenEvents, 59 | EventKey extends EventNames = EventNames, 60 | ListenMessageType = [T] extends [never] 61 | ? Parameter 62 | : T, 63 | EmitMessageArgs extends any[] = Parameters< 64 | EmitEvents[EventNames] 65 | >, 66 | //TODO: infer from last argument returntype(cb) of EmitEvents[EventKey] 67 | // if last argument is a function then infer return type 68 | // if last argument is not a function then infer void 69 | EmitMessageCbReturnType = any 70 | >( 71 | socket: EventKey | SocketLike>, 72 | event: 73 | | EventKey 74 | | (UseSocketEventOptions & UseSocketOptions), 75 | options?: UseSocketEventOptions 76 | ): UseSocketEventReturnType< 77 | ListenMessageType, 78 | EmitMessageArgs, 79 | EmitMessageCbReturnType 80 | > { 81 | let enabled = true; 82 | if (typeof socket === "string") { 83 | const _options = event as 84 | | (UseSocketEventOptions & UseSocketOptions) 85 | | undefined; 86 | options = _options; 87 | enabled = _options?.enabled ?? true; 88 | event = socket; 89 | socket = useSocket(_options).socket; 90 | } 91 | 92 | let onMessage; 93 | let keepPrevious; 94 | if (options) { 95 | onMessage = options.onMessage; 96 | keepPrevious = options.keepPrevious; 97 | } 98 | 99 | const ioContext = useContext>(IoContext); 100 | const { registerSharedListener, getConnection } = ioContext; 101 | const connection = enabled 102 | ? getConnection((socket as SocketLike).namespaceKey) 103 | : null; 104 | const [, rerender] = useState({}); 105 | const state = useRef<{ 106 | socket: SocketLike; 107 | status: "connecting" | "connected" | "disconnected"; 108 | error: Error | null; 109 | lastMessage: ListenMessageType; 110 | }>({ 111 | socket: connection?.socket || new SocketMock(), 112 | status: connection?.state.status || "disconnected", 113 | error: null, 114 | lastMessage: connection?.state.lastMessage[ 115 | event as string 116 | ] as ListenMessageType, 117 | }); 118 | 119 | const sendMessage = (...message: EmitMessageArgs) => 120 | new Promise((resolve, _reject) => { 121 | (socket as SocketLike).emit( 122 | event as string, 123 | ...message, 124 | (response: EmitMessageCbReturnType) => { 125 | resolve(response); 126 | } 127 | ); 128 | }); 129 | 130 | useEffect(() => { 131 | if (!connection) return; 132 | const cleanup = registerSharedListener( 133 | (socket as SocketLike).namespaceKey, 134 | event as string 135 | ); 136 | const unsubscribe = connection.subscribe((newState, _event) => { 137 | let changed = false; 138 | 139 | if (state.current.status !== newState.status) { 140 | state.current.status = newState.status; 141 | changed = true; 142 | } 143 | if (state.current.error !== newState.error) { 144 | state.current.error = newState.error; 145 | changed = true; 146 | } 147 | 148 | if ( 149 | _event === "message" && 150 | state.current.lastMessage !== newState.lastMessage[event as string] 151 | ) { 152 | const lastMessage = newState.lastMessage[event as string]; 153 | state.current.lastMessage = lastMessage; 154 | if (onMessage) { 155 | onMessage(lastMessage); 156 | } 157 | changed = true; 158 | } 159 | 160 | if (changed) { 161 | rerender({}); 162 | } 163 | }); 164 | return () => { 165 | unsubscribe(); 166 | if (!keepPrevious) { 167 | cleanup(); 168 | } 169 | }; 170 | }, [socket]); 171 | 172 | return { ...state.current, sendMessage }; 173 | } 174 | 175 | export default useSocketEvent; 176 | -------------------------------------------------------------------------------- /packages/socket.io-react-hook/src/utils/hash.ts: -------------------------------------------------------------------------------- 1 | /* 2 | shorthash 3 | (c) 2013 Bibig 4 | 5 | https://github.com/bibig/node-shorthash 6 | shorthash may be freely distributed under the MIT license. 7 | */ 8 | 9 | // refer to: http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ 10 | function bitwise(str) { 11 | let hash = 0; 12 | if (str.length == 0) return hash; 13 | for (let i = 0; i < str.length; i++) { 14 | const ch = str.charCodeAt(i); 15 | hash = (hash << 5) - hash + ch; 16 | hash = hash & hash; // Convert to 32bit integer 17 | } 18 | return hash; 19 | } 20 | 21 | // convert 10 binary to customized binary, max is 62 22 | function binaryTransfer(integer: number, binary) { 23 | binary = binary || 62; 24 | let stack: string[] = []; 25 | let num: number; 26 | let result = ""; 27 | let sign = integer < 0 ? "-" : ""; 28 | 29 | function table(num: number) { 30 | const t = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 31 | return t[num]; 32 | } 33 | 34 | integer = Math.abs(integer); 35 | 36 | while (integer >= binary) { 37 | num = integer % binary; 38 | integer = Math.floor(integer / binary); 39 | stack.push(table(num)); 40 | } 41 | 42 | if (integer > 0) { 43 | stack.push(table(integer)); 44 | } 45 | 46 | for (let i = stack.length - 1; i >= 0; i--) { 47 | result += stack[i]; 48 | } 49 | 50 | return sign + result; 51 | } 52 | 53 | /** 54 | * why choose 61 binary, because we need the last element char to replace the minus sign 55 | * eg: -aGtzd will be ZaGtzd 56 | */ 57 | export function unique(text: string) { 58 | const id = binaryTransfer(bitwise(text), 61); 59 | return id.replace("-", "Z"); 60 | } 61 | -------------------------------------------------------------------------------- /packages/socket.io-react-hook/src/utils/url.ts: -------------------------------------------------------------------------------- 1 | import parseuri from "parseuri"; 2 | 3 | type ParsedUrl = { 4 | source: string; 5 | protocol: string; 6 | authority: string; 7 | userInfo: string; 8 | user: string; 9 | password: string; 10 | host: string; 11 | port: string; 12 | relative: string; 13 | path: string; 14 | directory: string; 15 | file: string; 16 | query: string; 17 | anchor: string; 18 | pathNames: Array; 19 | queryKey: { [key: string]: string }; 20 | 21 | // Custom properties (not native to parseuri): 22 | id: string; 23 | href: string; 24 | }; 25 | 26 | /** 27 | * URL parser. 28 | * 29 | * @param uri - url 30 | * @param path - the request path of the connection 31 | * @param loc - An object meant to mimic window.location. 32 | * Defaults to window.location. 33 | * @public 34 | */ 35 | 36 | export function url( 37 | uri: string | ParsedUrl, 38 | path: string = "", 39 | defaultPort?: number | string 40 | ): ParsedUrl { 41 | let obj = uri as ParsedUrl; 42 | 43 | // default to window.location 44 | const loc = globalThis.location; 45 | if (null == uri) uri = loc.protocol + "//" + loc.host; 46 | 47 | // relative path support 48 | if (typeof uri === "string") { 49 | if ("/" === uri.charAt(0)) { 50 | if ("/" === uri.charAt(1)) { 51 | uri = loc.protocol + uri; 52 | } else { 53 | uri = loc.host + uri; 54 | } 55 | } 56 | 57 | if (!/^(https?|wss?):\/\//.test(uri)) { 58 | if ("undefined" !== typeof loc) { 59 | uri = loc.protocol + "//" + uri; 60 | } else { 61 | uri = "https://" + uri; 62 | } 63 | } 64 | 65 | // parse 66 | obj = parseuri(uri) as ParsedUrl; 67 | } 68 | 69 | // make sure we treat `localhost:80` and `localhost` equally 70 | if (!obj.port) { 71 | if (defaultPort) { 72 | obj.port = String(defaultPort); 73 | } else if (/^(http|ws)$/.test(obj.protocol)) { 74 | obj.port = "80"; 75 | } else if (/^(http|ws)s$/.test(obj.protocol)) { 76 | obj.port = "443"; 77 | } 78 | } 79 | 80 | obj.path = obj.path || "/"; 81 | 82 | const ipv6 = obj.host.indexOf(":") !== -1; 83 | const host = ipv6 ? "[" + obj.host + "]" : obj.host || "localhost"; 84 | 85 | // define unique id 86 | obj.id = obj.protocol + "://" + host + ":" + obj.port + path; 87 | // define href 88 | obj.href = 89 | obj.protocol + 90 | "://" + 91 | host + 92 | (loc && loc.port === obj.port ? "" : ":" + obj.port); 93 | 94 | return obj; 95 | } 96 | -------------------------------------------------------------------------------- /packages/socket.io-react-hook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "es5", 5 | "lib": [ 6 | "es6", 7 | "dom", 8 | "es2016", 9 | "es2017" 10 | ], 11 | "jsx": "react", 12 | "declaration": true, 13 | "moduleResolution": "node", 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "esModuleInterop": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noImplicitAny": false, 20 | "strictNullChecks": true, 21 | "suppressImplicitAnyIndexErrors": true, 22 | "allowSyntheticDefaultImports": true 23 | }, 24 | "include": [ 25 | "src" 26 | ], 27 | "exclude": [ 28 | "node_modules", 29 | "dist" 30 | ] 31 | } -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "examples/*" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /test-e2e.config.mjs: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "pipeline": { 4 | "build": { 5 | // A package's `build` script depends on that package's 6 | // dependencies and devDependencies 7 | // `build` tasks being completed first 8 | // (the `^` symbol signifies `upstream`). 9 | "dependsOn": ["^build"], 10 | // note: output globs are relative to each package's `package.json` 11 | // (and not the monorepo root) 12 | "outputs": ["dist/**"] 13 | }, 14 | "test": { 15 | // A package's `test` script depends on that package's 16 | // own `build` script being completed first. 17 | "dependsOn": ["build"], 18 | "outputs": [], 19 | // A package's `test` script should only be rerun when 20 | // either a `.tsx` or `.ts` file has changed in `src` or `test` folders. 21 | "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts", "test/**/*.tsx"] 22 | }, 23 | "lint": { 24 | // A package's `lint` script has no dependencies and 25 | // can be run whenever. It also has no filesystem outputs. 26 | "outputs": [] 27 | }, 28 | "deploy": { 29 | // A package's `deploy` script depends on the `build`, 30 | // `test`, and `lint` scripts of the same package 31 | // being completed. It also has no filesystem outputs. 32 | "dependsOn": ["build", "test", "lint"], 33 | "outputs": [] 34 | } 35 | } 36 | } 37 | --------------------------------------------------------------------------------