npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

4b-react-mdns

v0.8.1

Published

Discover mDNS/Bonjour servers on your LAN from a React overlay or CLI, and remember the one you pick.

Readme

4b-react-mdns

Let people pick a server off the local network from inside your React app, and remember which one they chose.

npm · source · changelog

Browsers can't speak mDNS — it's UDP multicast on :5353 — so discovery runs in Node and the React side consumes a small HTTP + Server-Sent-Events API. The package ships both halves: a Vite plugin (or standalone server) for the Node side, and a provider and hooks for the browser side.

Quick start

npm install 4b-react-mdns
npx 4bmdns config

config wires both halves — it adds mdnsPlugin() to your Vite config and wraps whatever your entry point renders in <MdnsProvider>, above every route:

  + vite config (vite.config.js) — added mdnsPlugin({ httpOnly: true })
  + entry point (src/main.jsx) — wrapped the rendered tree

Restart the dev server and a Scan network pill appears bottom-right. Pick a server and it's saved.

| Flag | | | --- | --- | | --http-only | Filter to _http._tcp / _https._tcp — writes httpOnly={true} | | --no-launcher / --launcher | Write launcher={false} / launcher={true} | | --dry-run | Show the plan without writing |

npx 4bmdns config --http-only --no-launcher
createRoot(document.getElementById('root')).render(
  <MdnsProvider launcher={false} httpOnly={true}>
    <StrictMode>
      <App />
    </StrictMode>
  </MdnsProvider>,
)

Use --no-launcher when you'd rather open the picker from your own button — see Your own button.

Re-running is safe. A bare config leaves an existing setup alone; pass a flag and it updates that prop in place, keeping every other prop on the tag (baseUrl, onSelect, position …) and the formatting. If it doesn't recognise a file it leaves it alone and prints the snippet to paste rather than guessing.

// vite.config.js
import { mdnsPlugin } from '4b-react-mdns/vite'

export default defineConfig({
  plugins: [react(), mdnsPlugin({ httpOnly: true })],
  server: { host: true }, // optional: reachable from a phone on the same Wi-Fi
})
// src/main.jsx
import { MdnsProvider } from '4b-react-mdns/react'

createRoot(document.getElementById('root')).render(
  <MdnsProvider httpOnly={true}>
    <App />
  </MdnsProvider>
)

Reading the pick

Anywhere inside the provider:

import { useMdns } from '4b-react-mdns/react'

function Player() {
  const { address, open } = useMdns()
  if (!address) return <button onClick={open}>Choose a server</button>
  return <video src={`http://${address}/stream`} />
}

Outside React — in a plain api client, say:

import { getSelectedAddress } from '4b-react-mdns/react'

const base = `http://${getSelectedAddress()}` // "192.168.0.224:3000"

The pick is stored in three places, and you can read any of them:

| Where | What | | --- | --- | | localStorage["4b_mDNS_ip"] | the plain ip:port string | | localStorage["4b_mDNS_selection"] | the full record as JSON | | ~/.scan-net/selection.json | the same record, on the machine running the scanner |

The file is the durable copy — it survives a cleared browser, is shared by every app pointing at that scanner, and is readable from shell scripts and Node.

<MdnsProvider>

Wraps your app and layers the picker over it. Children render untouched — the provider adds no wrapper element. The panel is portalled to <body> at z-index: 2147483000, so it sits above routed content no matter where the provider is mounted or what stacking contexts your app creates. Styles are injected at runtime and every selector is scoped under .snet-root, so there's no stylesheet to import and nothing bleeds either way.

Everything inside the panel is sized in em off a single custom property, so the whole thing scales as one piece. --snet-fs is flat at 15px up to roughly 1920px of viewport width and grows beyond it — a 4K screen at 100% scaling gets a panel sized for it instead of a postage stamp. Pin it to opt out:

.snet-root { --snet-fs: 15px }   /* or whatever size you want the panel to sit at */

| Prop | Default | | | --- | --- | --- | | baseUrl | /api | Where the scanner API is mounted | | httpOnly | server default | true = only _http/_https._tcp, false = every service. Omit to follow the CLI's --http-only flag | | filterToggle | false when httpOnly is set | Show the HTTP only switch in the panel | | launcher | true | Show the built-in launcher pill. false removes it entirely | | launcherLabel | Scan network | Its text | | position | bottom-right | Its corner — also bottom-left, top-right, top-left | | open | — | Pass a boolean to control visibility yourself | | onOpenChange | — | Called with true/false when the user opens or dismisses the panel | | defaultOpen | false | Starting state when open isn't passed | | closeOnEscape / closeOnBackdrop | true | Dismiss behaviour | | onSelect | — | Called with the record whenever the pick changes |

useMdns()

const {
  selection, ip, address, url,   // the pick
  select, clear,                 // change it
  services, connected, rescan,   // the scan
  httpOnly, setHttpOnly,         // the filter
  open, close, toggle, isOpen,   // the panel
} = useMdns()

Your own button

Drop the built-in pill and open the panel from anywhere:

function ServerButton() {
  const { open, address } = useMdns()
  return <button onClick={open}>{address ?? 'Pick a server'}</button>
}

<MdnsProvider launcher={false}>
  <Header><ServerButton /></Header>
  <Routes />
</MdnsProvider>

To hold the state yourself — driving it from a router, a menu, a keyboard shortcut — pass open and it becomes a controlled component:

const [scanning, setScanning] = useState(false)

<MdnsProvider launcher={false} open={scanning} onOpenChange={setScanning} httpOnly>
  <button onClick={() => setScanning(true)}>Scan network</button>
  <Routes />
</MdnsProvider>

While open is a boolean the provider never changes it on its own: Escape, the ✕ and a backdrop click all report through onOpenChange and leave the decision to you. Omit open and it manages itself again.

<MdnsList>

Renders the scanned services on your own page instead of behind the launcher:

<MdnsProvider httpOnly>
  <h2>Pick a streaming server</h2>
  <MdnsList />
</MdnsProvider>

| Prop | Default | | | --- | --- | --- | | httpOnly | provider's value | Override the filter for this list only | | query | '' | Substring filter over name, type, host, addresses | | limit | — | Show at most this many | | columns | true | false renders a single column | | emptyLabel | — | Text when nothing matches | | onSelect | — | Called after a card is picked |

Selecting from the list saves exactly as the overlay does. For full control over the markup, read useMdns().services and render the rows yourself.

Filtering to web servers

Most networks are noisy — printers, AirPlay targets, every laptop. httpOnly narrows the list to _http._tcp and _https._tcp:

mDNS: advertising "streaming-server" at Mac.local:3000 (_http._tcp.local)
mDNS: advertising "4bnode-app (4brains)" at 4brains.local:4021 (_http._tcp.local)

It's settable at every level, each overriding the one above:

npx 4bmdns --http-only              # the server default
<MdnsProvider httpOnly={true}>      {/* the app's default */}
<MdnsList httpOnly={false} />       {/* just this list */}

The HTTP only toggle in the panel flips it live. The UI streams every service and filters in the browser, which is what lets the toggle switch both ways instantly.

Passing httpOnly hides that toggle — the app has decided, so offering a switch that contradicts it only invites confusion. Pass filterToggle to bring it back:

<MdnsProvider httpOnly>                      {/* filtered, no toggle */}
<MdnsProvider httpOnly filterToggle>         {/* filtered, toggle shown */}
<MdnsProvider>                               {/* server default, toggle shown */}

Serving the API

The provider needs the scanner API on baseUrl. Three ways to get it there.

The Vite plugin covers npm run dev and npm run preview:

import { mdnsPlugin } from '4b-react-mdns/vite'

export default defineConfig({ plugins: [react(), mdnsPlugin({ httpOnly: true })] })

A separate process — for production, or when your app already serves its own /api/* and the routes would collide:

npx 4bmdns -p 4173
<MdnsProvider baseUrl="http://localhost:4173/api">

Embedded in your own server:

import { createServer } from '4b-react-mdns'

const { urls, storeFile, close } = await createServer({ port: 4173, httpOnly: true })

If the API isn't reachable the panel says so — a 404 reports that the plugin is missing rather than pretending the connection dropped.

CLI

4bmdns and scan-net are the same command.

npx 4bmdns              # start the scanner UI on :4173
npx 4bmdns config       # wire the scanner into the app in this directory
npx 4bmdns which        # print the selected server
npx 4bmdns forget       # clear it
$ 4bmdns which
streaming-server  ->  192.168.0.224:3000
http://192.168.0.224:3000

| Flag | Default | | | --- | --- | --- | | -p, --port <number> | 4173 | Port to listen on | | -H, --host <host> | 0.0.0.0 | Interface to bind | | --http-only | off | List only _http._tcp / _https._tcp | | --all | on | List every advertised service | | --store <dir> | ~/.scan-net | Where the selection is saved | | --dry-run | — | For config: show the plan without writing | | --json | — | Machine-readable output for which | | --no-open | — | Don't open a browser |

SCAN_NET_HOME overrides the store directory too, so you can scope a selection per project.

Node API

import { SelectionStore, createServer, MdnsScanner } from '4b-react-mdns'

const { ip, port, url } = new SelectionStore().read() ?? {}

selection.json looks like this:

{
  "id": "Living Room Pi._http._tcp.local",
  "name": "Living Room Pi",
  "kind": "Web server",
  "type": "http",
  "protocol": "tcp",
  "host": "livingroom.local",
  "ip": "192.168.1.42",
  "port": 8080,
  "address": "192.168.1.42:8080",
  "url": "http://livingroom.local:8080",
  "txt": null,
  "savedAt": "2026-09-09T10:31:07.482Z"
}

ip prefers IPv4, falls back to IPv6, then to the advertised .local hostname.

HTTP API

| Endpoint | | | --- | --- | | GET /api/status | { scanning, count, startedAt } | | GET /api/config | { httpOnly } — the server's default filter | | GET /api/services | services discovered so far; ?httpOnly= overrides the default | | GET /api/stream | SSE: snapshot, then up / update / down; ?httpOnly= too | | POST /api/scan | start scanning, or re-query to refresh | | POST /api/stop | stop scanning and drop results | | GET /api/selection | { selection, file } | | POST /api/selection | save a service as the selection | | DELETE /api/selection | clear it |

Using it from another device

With server: { host: true } (or the standalone CLI, which binds 0.0.0.0), the UI works from a phone or laptop on the same Wi-Fi at http://<your-ip>:5173. Selecting there writes to the JSON file on the machine running the scanner, which is usually what you want.

One caveat: a plain-HTTP LAN origin isn't a secure context, so browsers hide the async clipboard API entirely. Copy IP falls back to a legacy copy, and if that's blocked too it highlights the address so you can copy it manually. Clicking the address selects it as well.

There's no authentication on the API, so run it on networks you trust — or bind the standalone server to loopback with -H 127.0.0.1.

Requirements

Node 20+. React 18 or 19 as a peer dependency, needed only for the /react entry.

Develop

git clone https://git.4brains.in/shailesh.gautam/react-mdns-package.git
cd react-mdns-package
npm install
npm run dev      # Vite + the mDNS API on :5173
npm run build    # emit dist/ (standalone UI) and dist/react/ (the library)
npm start        # serve the built UI the way the published package does
npm run lint     # oxlint

npm run build runs twice: once for the standalone page in dist/, once for the embeddable library in dist/react/ with React left external. prepack runs it automatically, so npm publish always ships a current build.

To try unreleased changes in a host app, link the working copy and rebuild after each edit — Vite won't hot-reload a linked dependency's server code:

cd ../your-app && npm install ../react-mdns-package

Layout

| | | | --- | --- | | server/ | scanner, HTTP API, Vite plugin, standalone server, config codemod | | src/lib/ | the embeddable React entry — provider, hooks, panel, styles | | src/ | the standalone page served by the CLI | | bin/cli.js | 4bmdns / scan-net |

License

MIT