├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── CHANGELOG.md ├── LICENSE ├── index.tsx ├── package.json ├── readme.md ├── test.js └── tsconfig.json /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | commit-message: 9 | prefix: "ci" 10 | - package-ecosystem: npm 11 | directory: "/" 12 | schedule: 13 | interval: weekly 14 | open-pull-requests-limit: 10 15 | commit-message: 16 | prefix: "deps" 17 | ignore: 18 | - dependency-name: mitt 19 | versions: 20 | - 2.1.0 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Tests 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | node-version: [16.x, 18.x, 20.x, 22.x] 12 | react-version: [17.x, 18.x, 19.x] 13 | steps: 14 | - name: Checkout sources 15 | uses: actions/checkout@v4 16 | - name: Install Node.js ${{matrix.node-version}} 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{matrix.node-version}} 20 | - name: Install dependencies 21 | run: npm install 22 | - name: Install React ${{matrix.react-version}} 23 | if: matrix.react-version != '18.x' 24 | run: npm install -D react@${{matrix.react-version}} react-dom@${{matrix.react-version}} 25 | - name: Run tests 26 | run: npm test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goto-bus-stop/react-bus/154a0c230015d9557f19bcfefbc35e83bccb3b3e/.npmignore -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # react-bus change log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | This project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## 4.0.1 8 | * Fix ESM import resolution. (#38) 9 | 10 | Thanks [@barasimumatik](https://github.com/barasimumatik)! 11 | 12 | ## 4.0.0 13 | * Remove `BusContext` export. This is an implementation detail. 14 | * Update to mitt 3.x. 15 | * Add React 19 to the test matrix. 16 | 17 | Version 3.0.0 also works fine with React 19. You don't need to update to get React 19 support. 18 | 19 | ## 3.0.0 20 | * Add typescript types. 21 | * Update to mitt 2.x, which requires that browsers support the `Map` API. 22 | * Require React >= 17. 23 | 24 | All of this thanks to [@achmadk](https://github.com/achmadk)! 25 | 26 | ## 2.0.1 27 | * Remove `console.log`s. 28 | 29 | ## 2.0.0 30 | * Remove `withBus()`. 31 | * Add `useBus()` hook. 32 | * Add `useListener()` hook. 33 | * Require React >= 16.8. 34 | 35 | ## 1.0.3 36 | * Add missing `react` peer dependency. 37 | 38 | ## 1.0.2 39 | * Fix tests. 40 | * Add missing ES modules build to published package. 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Renée Kooi 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. 22 | -------------------------------------------------------------------------------- /index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import mitt, { type Emitter, type EventType, type Handler } from 'mitt' 3 | 4 | type Events = Record 5 | const BusContext = React.createContext | null>(null) 6 | 7 | /** Return the event emitter. */ 8 | export function useBus () { 9 | const bus = React.useContext(BusContext) 10 | if (!bus) throw new Error('useBus: missing context') 11 | return bus 12 | } 13 | 14 | /** 15 | * Attach an event listener to the bus while this component is mounted. 16 | * 17 | * Adds the listener after mount, and removes it before unmount. 18 | */ 19 | export function useListener (name: EventType, listener: Handler) { 20 | const bus = useBus() 21 | React.useEffect(() => { 22 | bus.on(name, listener) 23 | return () => { 24 | bus.off(name, listener) 25 | } 26 | }, [bus, name, listener]) 27 | } 28 | 29 | /** 30 | * Create an event emitter that will be available to all deeply nested child elements using the useBus() hook. 31 | */ 32 | export function Provider ({ children }: { children: React.ReactNode }) { 33 | const [bus] = React.useState(() => mitt()) 34 | return {children} 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-bus", 3 | "description": "A global event emitter for react.", 4 | "version": "4.0.1", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "exports": { 8 | "./package.json": "./package.json", 9 | ".": { 10 | "types": "./dist/index.d.mts", 11 | "require": "./dist/index.js", 12 | "default": "./dist/index.mjs" 13 | } 14 | }, 15 | "author": "Renée Kooi ", 16 | "bugs": { 17 | "url": "https://github.com/goto-bus-stop/react-bus/issues" 18 | }, 19 | "dependencies": { 20 | "@types/react": "^18.0.8", 21 | "mitt": "^3.0.1" 22 | }, 23 | "peerDependencies": { 24 | "react": ">=17.0.0 || ^19.0.0-0" 25 | }, 26 | "devDependencies": { 27 | "min-react-env": "^2.0.0", 28 | "react": "^18.0.0", 29 | "react-dom": "^18.0.0", 30 | "tsup": "^8.1.0", 31 | "typescript": "^5.0.2" 32 | }, 33 | "homepage": "https://github.com/goto-bus-stop/react-bus#readme", 34 | "keywords": [ 35 | "bus", 36 | "event-emitter", 37 | "eventemitter", 38 | "pubsub", 39 | "react" 40 | ], 41 | "license": "MIT", 42 | "repository": { 43 | "type": "git", 44 | "url": "git+https://github.com/goto-bus-stop/react-bus.git" 45 | }, 46 | "scripts": { 47 | "build": "tsup --format=esm --format=cjs --dts index.tsx index.tsx", 48 | "prepare": "npm run build", 49 | "test": "node --test" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # react-bus 2 | 3 | [![npm](https://badgen.net/npm/v/react-bus)](https://npmjs.com/package/react-bus) 4 | [![bundlephobia](https://badgen.net/bundlephobia/minzip/react-bus)](https://bundlephobia.com/result?p=react-bus) 5 | 6 | A global [event emitter](https://github.com/developit/mitt) for React apps. 7 | Useful if you need some user interaction in one place trigger an action in another place on the page, such as scrolling a logging element when pressing PageUp/PageDown in an input element (without having to store scroll position in state). 8 | 9 | ## Usage 10 | 11 | react-bus contains a `` component and a `useBus` hook. 12 | 13 | `` creates an event emitter and places it on the context. 14 | `useBus()` returns the event emitter from context. 15 | 16 | ```js 17 | import { Provider, useBus } from 'react-bus' 18 | // Use `bus` in . 19 | function ConnectedComponent () { 20 | const bus = useBus() 21 | } 22 | 23 | 24 | 25 | 26 | ``` 27 | 28 | For example, to communicate "horizontally" between otherwise unrelated components: 29 | 30 | ```js 31 | import { Provider as BusProvider, useBus, useListener } from 'react-bus' 32 | const App = () => ( 33 | 34 | 35 | 36 | 37 | ) 38 | 39 | function ScrollBox () { 40 | const el = React.useRef(null) 41 | const onscroll = React.useCallback(function (top) { 42 | el.current.scrollTop += top 43 | }, []) 44 | 45 | useListener('scroll', onscroll) 46 | 47 | return
48 | } 49 | 50 | // Scroll the ScrollBox when pageup/pagedown are pressed. 51 | function Input () { 52 | const bus = useBus() 53 | return 54 | 55 | function onkeydown (event) { 56 | if (event.key === 'PageUp') bus.emit('scroll', -200) 57 | if (event.key === 'PageDown') bus.emit('scroll', +200) 58 | } 59 | } 60 | ``` 61 | 62 | This may be easier to implement and understand than lifting the scroll state up into a global store. 63 | 64 | ## Install 65 | 66 | ``` 67 | npm install react-bus 68 | ``` 69 | 70 | ## API 71 | 72 | ### `` 73 | 74 | Create an event emitter that will be available to all deeply nested child elements using the `useBus()` hook. 75 | 76 | ### `const bus = useBus()` 77 | 78 | Return the event emitter. 79 | 80 | ### `useListener(name, fn)` 81 | 82 | Attach an event listener to the bus while this component is mounted. Adds the listener _after_ mount, and removes it before unmount. 83 | 84 | ## License 85 | 86 | [MIT](./LICENSE) 87 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var test = require('node:test') 3 | var assert = require('node:assert') 4 | var React = require('react') 5 | var ReactDOM = require('react-dom') 6 | require('min-react-env/install') 7 | var Provider = require('./').Provider 8 | var useBus = require('./').useBus 9 | var useListener = require('./').useListener 10 | 11 | var act = React.act || require('react-dom/test-utils').act 12 | 13 | function createTestRenderer () { 14 | var div = document.createElement('div') 15 | var root 16 | var reactMajor = parseInt((ReactDOM.version || '16').split('.')[0], 10) 17 | if (reactMajor >= 18) { 18 | var createRoot = require('react-dom/client').createRoot 19 | root = createRoot(div) 20 | } else { 21 | root = { 22 | render: function (element) { 23 | ReactDOM.render(element, div) 24 | }, 25 | unmount: function () { 26 | ReactDOM.unmountComponentAtNode(div) 27 | }, 28 | } 29 | } 30 | 31 | return root 32 | } 33 | 34 | var h = React.createElement 35 | 36 | test('emits events on context', function () { 37 | function onhello () { 38 | onhello.called = true 39 | } 40 | function Emitter (_props) { 41 | useBus().emit('hello') 42 | return h('div') 43 | } 44 | function Listener (_props) { 45 | useBus().on('hello', onhello) 46 | return h('div') 47 | } 48 | 49 | var renderer = createTestRenderer() 50 | act(function () { 51 | renderer.render( 52 | h(Provider, {}, 53 | h('div', {}, 54 | h(Listener), 55 | h(Emitter) 56 | ) 57 | ) 58 | ) 59 | }) 60 | 61 | assert(onhello.called) 62 | renderer.unmount() 63 | }) 64 | 65 | test('useListener', function () { 66 | function onhello () { 67 | onhello.called = true 68 | } 69 | function Emitter (_props) { 70 | const bus = useBus() 71 | React.useEffect(function () { 72 | bus.emit('hello') 73 | }) 74 | return h('div') 75 | } 76 | function Listener (_props) { 77 | useListener('hello', onhello) 78 | return h('div') 79 | } 80 | 81 | var renderer = createTestRenderer() 82 | act(function () { 83 | renderer.render( 84 | h(Provider, {}, 85 | h('div', {}, 86 | h(Listener), 87 | h(Emitter) 88 | ) 89 | ) 90 | ) 91 | }) 92 | 93 | assert(onhello.called) 94 | onhello.called = false 95 | act(function () { 96 | renderer.render( 97 | h(Provider, {}, 98 | h('div', {}, 99 | h(Emitter) 100 | ) 101 | ) 102 | ) 103 | }) 104 | assert(!onhello.called) 105 | 106 | renderer.unmount() 107 | }) 108 | 109 | test('esm', async () => { 110 | await import('react-bus') 111 | }) 112 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2015"], 4 | "moduleResolution": "node", 5 | "allowJs": true, 6 | "checkJs": true, 7 | "declaration": true, 8 | "emitDeclarationOnly": true, 9 | "jsx": "preserve", 10 | "strict": true, 11 | "esModuleInterop": true, 12 | "outDir": "types" 13 | }, 14 | "files": ["index.tsx"] 15 | } 16 | --------------------------------------------------------------------------------