├── .github └── workflows │ ├── codeql-analysis.yml │ ├── main.yml │ └── size.yml ├── .gitignore ├── LICENSE ├── README.md ├── errors └── no-provider.md ├── example ├── .npmignore ├── index.html ├── index.tsx ├── package.json ├── server.js ├── tsconfig.json └── yarn.lock ├── misc └── logo.png ├── package.json ├── src ├── RoomServiceProvider.tsx ├── contextForClient.tsx ├── errors.tsx ├── index.tsx ├── useList.tsx ├── useMap.tsx ├── usePresence.tsx └── useRoom.tsx ├── tsconfig.json └── yarn.lock /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # ******** NOTE ******** 12 | 13 | name: "CodeQL" 14 | 15 | on: 16 | push: 17 | branches: [ master ] 18 | pull_request: 19 | # The branches below must be a subset of the branches above 20 | branches: [ master ] 21 | schedule: 22 | - cron: '20 20 * * 4' 23 | 24 | jobs: 25 | analyze: 26 | name: Analyze 27 | runs-on: ubuntu-latest 28 | 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | language: [ 'javascript' ] 33 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 34 | # Learn more... 35 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v2 40 | 41 | # Initializes the CodeQL tools for scanning. 42 | - name: Initialize CodeQL 43 | uses: github/codeql-action/init@v1 44 | with: 45 | languages: ${{ matrix.language }} 46 | # If you wish to specify custom queries, you can do so here or in a config file. 47 | # By default, queries listed here will override any specified in a config file. 48 | # Prefix the list here with "+" to use these queries and those in the config file. 49 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 50 | 51 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 52 | # If this step fails, then you should remove it and run the build manually (see below) 53 | - name: Autobuild 54 | uses: github/codeql-action/autobuild@v1 55 | 56 | # ℹ️ Command-line programs to run using the OS shell. 57 | # 📚 https://git.io/JvXDl 58 | 59 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 60 | # and modify them (or add more) to build your code if your project 61 | # uses a compiled language 62 | 63 | #- run: | 64 | # make bootstrap 65 | # make release 66 | 67 | - name: Perform CodeQL Analysis 68 | uses: github/codeql-action/analyze@v1 69 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['10.x', '12.x', '14.x'] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - name: Test 26 | run: yarn test --ci --coverage --maxWorkers=2 27 | 28 | - name: Build 29 | run: yarn build 30 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: andresz1/size-limit-action@v1 11 | with: 12 | github_token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | dist 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Flaque 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | @roomservice/react 4 |
5 |
6 |
7 |

8 | 9 | This is the official React client for [Room Service](https://www.roomservice.dev/). Check out the [Documentation](https://www.roomservice.dev/docs/react-getting-started) for help getting started. Also join our official [Room Service Discord](https://discord.com/invite/VdPpp7Mx9u)! 10 | 11 | ## Install 12 | 13 | ``` 14 | yarn add @roomservice/react 15 | ``` 16 | 17 | ## Usage 18 | 19 | See [the documentation](https://docs.roomservice.dev/docs/guides/react). 20 | -------------------------------------------------------------------------------- /errors/no-provider.md: -------------------------------------------------------------------------------- 1 | # No Provider 2 | 3 | A hook is being used outside the RoomServiceProvider. 4 | 5 | ## Why you're getting this error 6 | 7 | You're using a Room Service hook such as `useMap`, `useList`, `usePresence` or `useRoom`, without 8 | a `RoomServiceProvider` setup higher up in your project. 9 | 10 | ### You may not have setup a `` 11 | 12 | Typically at the beginning of your project, in an `App.js` file, or `/pages/xyz.js` file, you'll 13 | need to setup the provider: 14 | 15 | ```tsx 16 | import { RoomServiceProvider } from '@roosmervice/react'; 17 | 18 | function App() { 19 | return ( 20 | 21 | {/* ... */} 22 | 23 | ); 24 | } 25 | ``` 26 | 27 | ### You may be using a hook inside the same component as `` 28 | 29 | Like other React libraries that use [context](https://reactjs.org/docs/context.html) providers, Room Service's hooks 30 | will only work in the _children_ of your provider. 31 | 32 | This does **NOT** work: 33 | 34 | ```tsx 35 | import { RoomServiceProvider, useMap } from '@roosmervice/react'; 36 | 37 | function App() { 38 | // BAD 39 | const [map, setMap] = useMap('myroom', 'mymap'); 40 | 41 | return ( 42 | 43 | {/* ... */} 44 | 45 | ); 46 | } 47 | ``` 48 | 49 | This **DOES** work: 50 | 51 | ```tsx 52 | function TheRestOfYourProject() { 53 | // The hook works here, because it's referring to a component 54 | // within the provider's tree. 55 | const [map, setMap] = useMap('myroom', 'mymap'); 56 | if (!map) return null; 57 | 58 | return
{map.get('some-key')}
; 59 | } 60 | 61 | function App() { 62 | return ( 63 | 64 | 65 | 66 | ); 67 | } 68 | ``` 69 | 70 | ### You may be using a hook outside of the provider's tree 71 | 72 | You may have your provider setup not on the top level of your project, but farther 73 | down within the project. If you are, be sure that your hooks are used within the tree 74 | of the provider, and not adjacent to it. 75 | 76 | For example, this won't work: 77 | 78 | ```tsx 79 | import { RoomServiceProvider } from '@roosmervice/react'; 80 | 81 | function Adjacent() { 82 | // BAD: This won't work! 83 | const [map, setMap] = useMap("myroom", "mymap") 84 | 85 | if (!map) return null; 86 | return
{map.get('some-key')}
; 87 | } 88 | 89 | function App() { 90 | return ( 91 |
92 |
93 | 94 |
95 | 96 | {/* ... */} 97 | 98 |
99 | ); 100 | } 101 | ``` 102 | -------------------------------------------------------------------------------- /example/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | dist -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Playground 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import 'react-app-polyfill/ie11'; 2 | import * as React from 'react'; 3 | import * as ReactDOM from 'react-dom'; 4 | import { RoomServiceProvider, useMap, usePresence } from '../.'; 5 | 6 | function Input(props: { name: string; map: string }) { 7 | const [json, map] = useMap('room', props.map); 8 | const rerenders = React.useRef(0); 9 | rerenders.current++; 10 | 11 | function onChange(key, e) { 12 | if (!map) return; 13 | map.set(key, e.target.value); 14 | } 15 | 16 | return ( 17 | <> 18 | onChange(props.name, e)} />{' '} 19 | {rerenders.current} 20 | 21 | ); 22 | } 23 | 24 | const MapDemo = () => { 25 | return ( 26 |
27 | 38 |
39 | ); 40 | }; 41 | 42 | const useInterval = (callback, delay) => { 43 | const savedCallback = React.useRef() as any; 44 | 45 | React.useEffect(() => { 46 | savedCallback.current = callback; 47 | }, [callback]); 48 | 49 | React.useEffect(() => { 50 | function tick() { 51 | savedCallback.current(); 52 | } 53 | if (delay !== null) { 54 | let id = setInterval(tick, delay); 55 | return () => clearInterval(id); 56 | } 57 | }, [delay]); 58 | }; 59 | 60 | const PresenceDemo = () => { 61 | const [first, firstClient] = usePresence('room', 'positions1'); 62 | const [second, secondClient] = usePresence('room', 'positions2'); 63 | 64 | useInterval(() => { 65 | firstClient.set(new Date().toTimeString()); 66 | secondClient.set(new Date().toTimeString()); 67 | }, 1000); 68 | 69 | return ( 70 |
71 |

{JSON.stringify(first)}

72 |

{JSON.stringify(second)}

73 |
74 | ); 75 | }; 76 | 77 | const Wrapper = () => { 78 | return ( 79 |
80 | 83 | 84 | 85 | 86 |
87 | ); 88 | }; 89 | 90 | ReactDOM.render(, document.getElementById('root')); 91 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "parcel index.html", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "cors": "^2.8.5", 12 | "express": "^4.17.1", 13 | "node-fetch": "^2.6.1", 14 | "react-app-polyfill": "^1.0.0" 15 | }, 16 | "alias": { 17 | "react": "../node_modules/react", 18 | "react-dom": "../node_modules/react-dom/profiling", 19 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^16.9.11", 23 | "@types/react-dom": "^16.8.4", 24 | "parcel": "^1.12.3", 25 | "typescript": "^3.4.5" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | // server.js 2 | 3 | const express = require('express'); 4 | const fetch = require('node-fetch'); 5 | const cors = require('cors'); 6 | 7 | const app = express(); 8 | const port = 3002; 9 | 10 | app.use(express.json()); 11 | app.use(cors()); 12 | 13 | const API_KEY = 'XYEni4263vYFSPBMp345o'; 14 | 15 | app.post('/roomservice', async (req, res) => { 16 | // In practice, this should be whatever user id YOU use. 17 | const user = Math.random() 18 | .toString(36) 19 | .substr(2, 9); 20 | 21 | const body = req.body; 22 | 23 | const r = await fetch('https://super.roomservice.dev/provision', { 24 | method: 'post', 25 | 26 | headers: { 27 | Authorization: `Bearer ${API_KEY}`, 28 | 'Content-Type': 'application/json', 29 | }, 30 | 31 | body: JSON.stringify({ 32 | user: user, 33 | resources: body.resources, 34 | }), 35 | }); 36 | 37 | return res.json(await r.json()); 38 | }); 39 | 40 | app.listen(port, () => { 41 | console.log(`Example app listening at http://localhost:${port}`); 42 | }); 43 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": false, 4 | "target": "es5", 5 | "module": "commonjs", 6 | "jsx": "react", 7 | "moduleResolution": "node", 8 | "noImplicitAny": false, 9 | "noUnusedLocals": false, 10 | "noUnusedParameters": false, 11 | "removeComments": true, 12 | "strictNullChecks": true, 13 | "preserveConstEnums": true, 14 | "sourceMap": true, 15 | "lib": ["es2015", "es2016", "dom"], 16 | "types": ["node"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /misc/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getroomservice/react/a94ad8f96e9bead2093f82c719d32c2a90af20ca/misc/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.6", 3 | "license": "MIT", 4 | "main": "dist/index.js", 5 | "typings": "dist/index.d.ts", 6 | "files": [ 7 | "dist", 8 | "src" 9 | ], 10 | "engines": { 11 | "node": ">=10" 12 | }, 13 | "scripts": { 14 | "start": "tsdx watch", 15 | "build": "tsdx build", 16 | "test": "tsdx test --passWithNoTests", 17 | "prepare": "tsdx build", 18 | "size": "size-limit", 19 | "analyze": "size-limit --why" 20 | }, 21 | "peerDependencies": { 22 | "react": ">=16" 23 | }, 24 | "prettier": { 25 | "printWidth": 80, 26 | "semi": true, 27 | "singleQuote": true, 28 | "trailingComma": "es5" 29 | }, 30 | "name": "@roomservice/react", 31 | "author": "Flaque", 32 | "module": "dist/react.esm.js", 33 | "size-limit": [ 34 | { 35 | "path": "dist/react.cjs.production.min.js", 36 | "limit": "10 KB" 37 | }, 38 | { 39 | "path": "dist/react.esm.js", 40 | "limit": "10 KB" 41 | } 42 | ], 43 | "devDependencies": { 44 | "@size-limit/preset-small-lib": "^4.6.0", 45 | "@types/react": "^16.9.52", 46 | "@types/react-dom": "^16.9.8", 47 | "husky": "^4.3.0", 48 | "react": "^16.13.1", 49 | "react-dom": "^16.13.1", 50 | "size-limit": "^4.6.0", 51 | "tsdx": "^0.14.1", 52 | "tslib": "^2.0.3", 53 | "typescript": "^4.0.3" 54 | }, 55 | "dependencies": { 56 | "@roomservice/browser": "^3.0.4" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/RoomServiceProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ClientProvider } from './contextForClient'; 3 | import { RoomServiceParameters } from '@roomservice/browser'; 4 | 5 | export function RoomServiceProvider(props: { 6 | children: React.ReactNode; 7 | clientParameters: RoomServiceParameters; 8 | // Whether to authenticate and connect to RoomService infrastructure. Can be 9 | // initially set to false to delay authentication until the user is logged in. 10 | // Defaults to true. 11 | online?: boolean; 12 | }) { 13 | return ( 14 | 18 | {props.children} 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/contextForClient.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | RoomClient, 4 | RoomService, 5 | RoomServiceParameters, 6 | } from '@roomservice/browser'; 7 | import { createContext, ReactNode, useRef } from 'react'; 8 | 9 | interface ClientContext { 10 | addRoom?: (key: string) => Promise; 11 | } 12 | 13 | export const clientContext = createContext({}); 14 | 15 | export function ClientProvider({ 16 | children, 17 | clientParameters, 18 | online, 19 | }: { 20 | children: ReactNode; 21 | clientParameters: RoomServiceParameters; 22 | online?: boolean; 23 | }) { 24 | const rs = useRef | undefined>(); 25 | 26 | const delayedInitClientTriggers = useRef void>>([]); 27 | if (online === undefined || online === true) { 28 | rs.current = new RoomService(clientParameters); 29 | for (const trigger of delayedInitClientTriggers.current) { 30 | trigger(); 31 | } 32 | delayedInitClientTriggers.current = []; 33 | } 34 | 35 | // ref instead of state here to prevent a double render 36 | // and allow delayed initialization 37 | const pendingRef = useRef<{ [key: string]: Promise }>({}); 38 | 39 | async function addRoom(key: string): Promise { 40 | // Make sure rs.room is only ever called once per room 41 | if (!pendingRef.current[key]) { 42 | if (rs.current === undefined) { 43 | const { trigger, promise } = triggeredPromise(); 44 | delayedInitClientTriggers.current.push(trigger); 45 | pendingRef.current[key] = promise.then(() => rs.current!.room(key)); 46 | } else { 47 | pendingRef.current[key] = rs.current.room(key); 48 | } 49 | } 50 | const room = await pendingRef.current[key]; 51 | return room; 52 | } 53 | 54 | return ( 55 | 60 | {children} 61 | 62 | ); 63 | } 64 | 65 | function triggeredPromise(): { 66 | trigger: () => void; 67 | promise: Promise; 68 | } { 69 | let trigger: (_: any) => void; 70 | const triggerPromise = new Promise(resolve => { 71 | trigger = resolve; 72 | }); 73 | 74 | return { trigger: trigger! as () => void, promise: triggerPromise }; 75 | } 76 | -------------------------------------------------------------------------------- /src/errors.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Occurs if the programmer uses a hook in a component that doesn't 3 | * have the RoomServiceProvider as an ancestor. 4 | */ 5 | export const errOutsideOfProvider = () => 6 | new Error( 7 | 'A hook is being used outside the RoomServiceProvider. Learn more: https://err.sh/getroomservice/react/no-provider' 8 | ); 9 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export { usePresence } from './usePresence'; 2 | export { useMap } from './useMap'; 3 | export { useList } from './useList'; 4 | export { useRoom } from './useRoom'; 5 | export { RoomServiceProvider } from './RoomServiceProvider'; 6 | export { 7 | ListClient, 8 | MapClient, 9 | PresenceClient, 10 | RoomServiceParameters, 11 | RoomClient, 12 | } from '@roomservice/browser'; 13 | -------------------------------------------------------------------------------- /src/useList.tsx: -------------------------------------------------------------------------------- 1 | import { useRoom } from './useRoom'; 2 | import { ListClient } from '@roomservice/browser'; 3 | import { useState, useEffect } from 'react'; 4 | 5 | export function useList>( 6 | roomName: string, 7 | listName: string 8 | ): [T, ListClient | undefined] { 9 | const [obj, setObj] = useState([]); 10 | const [list, setList] = useState>(); 11 | const room = useRoom(roomName); 12 | 13 | useEffect(() => { 14 | if (!room) return; 15 | 16 | const l = room.list(listName); 17 | setObj(l.toArray() as any); 18 | setList(l); 19 | 20 | const subscription = room.subscribe(l, next => { 21 | setObj(next); 22 | }); 23 | 24 | return () => room.unsubscribe(subscription); 25 | }, [room, listName]); 26 | 27 | return [obj || [], list]; 28 | } 29 | -------------------------------------------------------------------------------- /src/useMap.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { MapClient } from '@roomservice/browser'; 3 | import { useRoom } from './useRoom'; 4 | 5 | type MapObject = { [key: string]: any }; 6 | 7 | export function useMap( 8 | roomName: string, 9 | mapName: string 10 | ): [T, MapClient | undefined] { 11 | const [obj, setObj] = useState({} as T); 12 | const [map, setMap] = useState>(); 13 | const room = useRoom(roomName); 14 | 15 | useEffect(() => { 16 | if (!room) return; 17 | 18 | const m = room!.map(mapName); 19 | setObj(m.toObject() as any); 20 | setMap(m); 21 | 22 | const subscription = room.subscribe(m, obj => { 23 | setObj(obj); 24 | }); 25 | 26 | return () => room.unsubscribe(subscription); 27 | }, [room, mapName]); 28 | 29 | return [obj, map]; 30 | } 31 | -------------------------------------------------------------------------------- /src/usePresence.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef, useCallback } from 'react'; 2 | import { PresenceClient } from '@roomservice/browser'; 3 | import { useRoom } from './useRoom'; 4 | 5 | export function usePresence( 6 | roomName: string, 7 | key: string 8 | ): [{ [key: string]: T }, PresenceClient] { 9 | const presence = useRef>(); 10 | const [val, setVal] = useState<{ [key: string]: T }>({}); 11 | const room = useRoom(roomName); 12 | 13 | useEffect(() => { 14 | if (!room) return; 15 | 16 | const p = room!.presence(key); 17 | presence.current = p; 18 | 19 | // Empty buffer 20 | if (buffer.current !== undefined) { 21 | presence.current.set(buffer.current[0], buffer.current[1]); 22 | buffer.current = undefined; 23 | } 24 | 25 | setVal(p.getAll()); 26 | 27 | const subscription = room!.subscribe(p, val => { 28 | setVal(val); 29 | }); 30 | 31 | return () => room!.unsubscribe(subscription); 32 | }, [room, key]); 33 | 34 | // a programmer can technically write to the presence key before 35 | // we connect to the room, this keeps track of that. 36 | const buffer = useRef<[T, number?] | undefined>(undefined); 37 | 38 | const bufferedSet = useCallback((value: T, expiresAfter?: number): any => { 39 | // Buffer before the room is open 40 | if (!presence.current) { 41 | buffer.current = [value, expiresAfter]; 42 | return; 43 | } 44 | presence.current?.set(value, expiresAfter); 45 | }, []); 46 | 47 | // useState here so object stays the same, preventing spurious re-renders 48 | const wrapper = useRef>({ 49 | set: bufferedSet, 50 | getMine: () => { 51 | return presence.current?.getMine(); 52 | }, 53 | getAll: () => { 54 | return presence.current?.getAll() ?? {}; 55 | }, 56 | }); 57 | 58 | return [val, wrapper.current]; 59 | } 60 | -------------------------------------------------------------------------------- /src/useRoom.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from 'react'; 2 | import { RoomClient } from '@roomservice/browser'; 3 | import { clientContext } from './contextForClient'; 4 | import { errOutsideOfProvider } from './errors'; 5 | 6 | export function useRoom(roomName: string): RoomClient | undefined { 7 | const ctx = useContext(clientContext); 8 | if (!ctx.addRoom) { 9 | throw errOutsideOfProvider(); 10 | } 11 | const [room, setRoom] = useState(); 12 | 13 | useEffect(() => { 14 | async function load() { 15 | setRoom(await ctx!.addRoom!(roomName)); 16 | } 17 | 18 | load().catch(console.error); 19 | }, [roomName]); 20 | 21 | return room; 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // transpile JSX to React.createElement 25 | "jsx": "react", 26 | // interop between ESM and CJS modules. Recommended by TS 27 | "esModuleInterop": true, 28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 29 | "skipLibCheck": true, 30 | // error out if import and file system have a casing mismatch. Recommended by TS 31 | "forceConsistentCasingInFileNames": true, 32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 33 | "noEmit": true, 34 | } 35 | } 36 | --------------------------------------------------------------------------------