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

rxor

v2.0.0

Published

Fine-grained reactive signals for React

Readme

rxor

Fine-grained reactive signals for React.

Zero dependency core. ~2KB gzipped. Works with React 18+ and React 19.

rxor brings reactive signals to React, inspired by Angular Signals, Vue 3 ref/computed, and SolidJS.


Installation

npm install rxor
# or
pnpm add rxor
# or
yarn add rxor

Requirements: React 18.0+ and TypeScript 5.0+

Compatibility: Works with any React UI library (Mantine, MUI, Chakra, Ant Design, etc.).


Quick start

import { signal, computed, SignalValue } from 'rxor'

const count = signal(0)
const doubled = computed(() => count.value * 2)

function Counter() {
  // This component renders ONCE and never re-renders
  return (
    <div>
      <p>Count: <SignalValue signal={count} /></p>
      <p>Doubled: <SignalValue signal={doubled} /></p>
      <button onClick={() => count.value++}>+1</button>
    </div>
  )
}

When count changes, only the two <SignalValue> texts update. The Counter function never re-runs. The <button> never re-renders.


Two ways to read a signal in React

rxor provides two approaches. Choose based on your needs.

useSignal — simple, the component re-renders

import { signal, useSignal } from 'rxor'

const name = signal("John")

function Greeting() {
  const n = useSignal(name)  // the component re-renders when name changes
  return <p>Hello, {n}!</p>
}

Use useSignal when you need the value as a variable (for logic, props, conditions, loops).

<SignalValue> — fine-grained, the component never re-renders

import { signal, SignalValue } from 'rxor'

const name = signal("John")

function Greeting() {
  // No hook, no re-render. This function runs ONCE.
  return <p>Hello, <SignalValue signal={name} />!</p>
}

Use <SignalValue> when you just need to display a value (text, number). It creates a micro-component that updates independently.

Do not mix both for the same signal

// BAD — redundant, the component re-renders AND SignalValue re-renders
const n = useSignal(name)
<p><SignalValue signal={name} /></p>

// GOOD — pick one
const n = useSignal(name)        // Option A: component re-renders
<p>{n}</p>

<p><SignalValue signal={name} /></p>  // Option B: only the text re-renders

When to use which?

| Situation | Use | |---|---| | Display a text/number in JSX | <SignalValue> | | Pass a value as prop to a component | useSignal | | Use a value in a condition or loop | useSignal | | Maximum performance, zero re-renders | <SignalValue> | | Simple and quick | useSignal |


Core API

signal<T>(initial): Signal<T>

A reactive container. Reading .value tracks dependencies. Writing .value notifies subscribers.

import { signal } from 'rxor/core'

const name = signal("John")

name.value          // read: "John"
name.value = "Jane" // write: notifies all subscribers
name.peek()         // read without tracking: "Jane"

Supported types

// Primitives
const count = signal(0)                       // Signal<number>
const label = signal("hello")                 // Signal<string>
const active = signal(true)                   // Signal<boolean>
const maybe = signal<string | null>(null)     // Signal<string | null>

// Objects — each property is tracked independently
const user = signal({ name: "John", age: 25 })
user.value.name = "Jane"  // notifies only watchers of .name, not .age

// Arrays — mutations are intercepted
const list = signal([1, 2, 3])
list.value.push(4)        // notifies watchers
list.value.splice(0, 1)   // notifies watchers
list.value[0] = 99        // notifies watchers

// Map
const cache = signal(new Map<string, number>())
cache.value.set("key", 42)    // notifies watchers
cache.value.delete("key")     // notifies watchers

// Set
const tags = signal(new Set<string>())
tags.value.add("urgent")      // notifies watchers
tags.value.delete("urgent")   // notifies watchers

Deep reactivity

When a signal holds an object, each property is tracked independently:

const state = signal({ a: { x: 1 }, b: { y: 2 } })

effect(() => {
  console.log(state.value.a.x)  // tracks only a.x
})

state.value.b.y = 99  // does NOT re-run the effect
state.value.a.x = 10  // re-runs the effect

| Method | Description | |---|---| | .value | Read (with tracking) or write the value | | .peek() | Read without creating a dependency | | .subscribe(cb) | Listen for changes, returns an unsubscribe function |


computed<T>(fn): Computed<T>

A derived value that recalculates automatically when its dependencies change.

import { signal, computed } from 'rxor/core'

const price = signal(100)
const tax = signal(0.2)
const total = computed(() => price.value * (1 + tax.value))

total.value   // 120
price.value = 200
total.value   // 240 — recalculated automatically

Key behaviors:

  • Lazy — does not compute until .value is read
  • Cached — does not recompute if dependencies haven't changed
  • Readonly — setting .value throws an error
  • Nested — a computed can depend on other computeds
const firstName = signal("John")
const lastName = signal("Doe")
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
const greeting = computed(() => `Hello, ${fullName.value}!`)

greeting.value   // "Hello, John Doe!"
firstName.value = "Jane"
greeting.value   // "Hello, Jane Doe!"

effect(fn): () => void

Runs a function immediately, then re-runs it whenever its dependencies change. Returns a dispose function.

import { signal, effect } from 'rxor/core'

const count = signal(0)

const dispose = effect(() => {
  console.log("Count:", count.value)
})
// logs: "Count: 0"

count.value = 5
// logs: "Count: 5"

dispose()
count.value = 10  // nothing happens

Replaces most useEffect usage

// Sync with localStorage
effect(() => {
  localStorage.setItem("theme", theme.value)
})

// Update document title
effect(() => {
  document.title = `(${unreadCount.value}) Messages`
})

// Log changes
effect(() => {
  console.log("User changed:", user.value)
})

Cleanup

Return a function from the effect for cleanup before each re-run:

const userId = signal(1)

effect(() => {
  const ws = new WebSocket(`/ws/user/${userId.value}`)
  return () => ws.close()  // cleanup before re-run
})

Dynamic dependencies

Effects automatically re-track dependencies on each run:

const toggle = signal(true)
const a = signal("A")
const b = signal("B")

effect(() => {
  console.log(toggle.value ? a.value : b.value)
})

b.value = "B2"       // does NOT re-run (toggle is true, b not tracked)
toggle.value = false  // re-runs, logs "B2"
a.value = "A2"        // does NOT re-run (toggle is false, a not tracked)

batch(fn)

Groups multiple signal writes into a single notification:

import { signal, effect, batch } from 'rxor/core'

const a = signal(1)
const b = signal(2)

effect(() => {
  console.log(a.value + b.value)
})
// logs: 3

batch(() => {
  a.value = 10
  b.value = 20
})
// logs: 30 (once, not twice)

untracked(fn)

Read signal values without creating dependencies:

import { signal, effect, untracked } from 'rxor/core'

const count = signal(0)
const label = signal("hello")

effect(() => {
  const c = count.value                       // tracked
  const l = untracked(() => label.value)      // NOT tracked
  console.log(c, l)
})

label.value = "world"  // does NOT re-run the effect
count.value = 1        // re-runs the effect

React hooks

useSignal<T>(signal): T

Subscribe to a signal. The component re-renders when the signal changes.

Uses useSyncExternalStore — concurrent mode safe and SSR compatible.

import { signal, computed, useSignal } from 'rxor'

const count = signal(0)
const doubled = computed(() => count.value * 2)

function Display() {
  const c = useSignal(count)
  const d = useSignal(doubled)
  return <p>{c} x2 = {d}</p>
}

useComputed<T>(fn): T

Create a computed inline in a component:

import { signal, useComputed } from 'rxor'

const price = signal(100)
const quantity = signal(3)

function Total() {
  const total = useComputed(() => price.value * quantity.value)
  return <p>Total: {total}</p>
}

Store

createStore(definition)

Group signals, computed, and actions into a typed store:

import { signal, computed, createStore } from 'rxor'

const count = signal(0)

export const counterStore = createStore({
  count,
  doubled: computed(() => count.value * 2),
  increment() { count.value++ },
  decrement() { count.value-- },
  reset() { count.value = 0 },
})

useStore(store, selector)

Subscribe to a specific signal from a store. The component only re-renders when that signal changes:

import { useStore } from 'rxor'
import { counterStore } from '../store/counterStore'

function Counter() {
  const count = useStore(counterStore, s => s.count)
  const doubled = useStore(counterStore, s => s.doubled)

  return (
    <div>
      <p>Count: {count}</p>
      <p>Doubled: {doubled}</p>
      <button onClick={counterStore.increment}>+1</button>
    </div>
  )
}

Architecture: Services

rxor does not include a service system in the package. You don't need one — a TypeScript class with signals is a service.

Why services?

In React, business logic often ends up inside components. With rxor, you separate concerns:

  • Service = business logic, data, API calls
  • Component = reads and displays, no logic

This is the same architecture as Angular services, but without decorators or dependency injection framework.

Creating a service

// service/UserService.ts
import { signal, computed } from 'rxor/core'

type User = { id: number; name: string; role: string }

export class UserService {
  // Private state — components cannot write directly
  private readonly _users = signal<User[]>([])
  private readonly _loading = signal(false)
  private readonly _error = signal<string | null>(null)
  private readonly _search = signal("")

  // Public state — read only
  readonly loading = this._loading
  readonly error = this._error
  readonly search = this._search

  readonly users = computed(() => {
    const s = this._search.value.toLowerCase()
    if (!s) return this._users.value
    return this._users.value.filter(u => u.name.toLowerCase().includes(s))
  })

  readonly count = computed(() => this.users.value.length)

  // Actions
  async loadUsers() {
    this._loading.value = true
    this._error.value = null
    try {
      const res = await fetch("/api/users")
      this._users.value = await res.json()
    } catch (e) {
      this._error.value = (e as Error).message
    } finally {
      this._loading.value = false
    }
  }

  addUser(name: string, role: string) {
    this._users.value = [...this._users.value, { id: Date.now(), name, role }]
  }

  removeUser(id: number) {
    this._users.value = this._users.value.filter(u => u.id !== id)
  }

  setSearch(value: string) {
    this._search.value = value
  }
}

Instantiating services

// service/index.ts
import { UserService } from './UserService'
import { AuthService } from './AuthService'

export const userService = new UserService()
export const authService = new AuthService()

Using in a component

The component is "stupid" — it reads and displays, nothing else:

// components/UserTable.tsx
import { useSignal } from 'rxor/react'
import { userService } from '../service'
import { useEffect } from 'react'

export function UserTable() {
  const users = useSignal(userService.users)
  const loading = useSignal(userService.loading)
  const error = useSignal(userService.error)
  const count = useSignal(userService.count)

  useEffect(() => { userService.loadUsers() }, [])

  if (loading) return <p>Loading...</p>
  if (error) return <p>Error: {error}</p>

  return (
    <div>
      <h2>Users ({count})</h2>
      <input
        placeholder="Search..."
        onChange={e => userService.setSearch(e.target.value)}
      />
      <ul>
        {users.map(user => (
          <li key={user.id}>
            {user.name} ({user.role})
            <button onClick={() => userService.removeUser(user.id)}>X</button>
          </li>
        ))}
      </ul>
    </div>
  )
}

Service with pagination

// service/ProductService.ts
import { signal, computed } from 'rxor/core'

type Product = { id: number; name: string; price: number }

type PaginationMeta = {
  page: number
  pageSize: number
  totalItems: number
  totalPages: number
}

export class ProductService {
  private readonly _products = signal<Product[]>([])
  private readonly _loading = signal(false)
  private readonly _error = signal<string | null>(null)
  private readonly _pagination = signal<PaginationMeta>({
    page: 1,
    pageSize: 20,
    totalItems: 0,
    totalPages: 0,
  })

  readonly products = this._products
  readonly loading = this._loading
  readonly error = this._error
  readonly pagination = this._pagination
  readonly hasNextPage = computed(() => this._pagination.value.page < this._pagination.value.totalPages)
  readonly hasPrevPage = computed(() => this._pagination.value.page > 1)

  async loadProducts(page = 1) {
    this._loading.value = true
    this._error.value = null
    try {
      const size = this._pagination.value.pageSize
      const res = await fetch(`/api/products?page=${page}&size=${size}`)
      const data = await res.json()
      this._products.value = data.items
      this._pagination.value = {
        page: data.page,
        pageSize: data.pageSize,
        totalItems: data.total,
        totalPages: Math.ceil(data.total / data.pageSize),
      }
    } catch (e) {
      this._error.value = (e as Error).message
    } finally {
      this._loading.value = false
    }
  }

  nextPage() {
    if (this.hasNextPage.value) {
      this.loadProducts(this._pagination.value.page + 1)
    }
  }

  prevPage() {
    if (this.hasPrevPage.value) {
      this.loadProducts(this._pagination.value.page - 1)
    }
  }
}
// components/ProductList.tsx
import { useSignal } from 'rxor/react'
import { productService } from '../service'
import { useEffect } from 'react'

export function ProductList() {
  const products = useSignal(productService.products)
  const loading = useSignal(productService.loading)
  const pagination = useSignal(productService.pagination)
  const hasNext = useSignal(productService.hasNextPage)
  const hasPrev = useSignal(productService.hasPrevPage)

  useEffect(() => { productService.loadProducts() }, [])

  if (loading) return <p>Loading...</p>

  return (
    <div>
      <h2>Products (page {pagination.page} / {pagination.totalPages})</h2>

      <ul>
        {products.map(p => (
          <li key={p.id}>{p.name} — {p.price}$</li>
        ))}
      </ul>

      <button disabled={!hasPrev} onClick={() => productService.prevPage()}>Previous</button>
      <span> Page {pagination.page} of {pagination.totalPages} </span>
      <button disabled={!hasNext} onClick={() => productService.nextPage()}>Next</button>
    </div>
  )
}

Service-to-service communication

Services can depend on each other via constructor injection:

// service/OrderService.ts
import { signal } from 'rxor/core'
import type { AuthService } from './AuthService'

export class OrderService {
  constructor(private auth: AuthService) {}

  private readonly _orders = signal([])
  readonly orders = this._orders

  async placeOrder(productId: number) {
    if (!this.auth.isLoggedIn.value) {
      throw new Error("Not authenticated")
    }
    // ...
  }
}
// service/index.ts
import { AuthService } from './AuthService'
import { UserService } from './UserService'
import { OrderService } from './OrderService'

export const authService = new AuthService()
export const userService = new UserService()
export const orderService = new OrderService(authService)

Recommended project structure

src/
├── service/
│   ├── AuthService.ts
│   ├── UserService.ts
│   ├── ProductService.ts
│   ├── OrderService.ts
│   └── index.ts              ← instantiation
├── components/
│   ├── UserTable.tsx          ← reads userService
│   ├── ProductList.tsx        ← reads productService
│   ├── LoginForm.tsx          ← reads authService
│   └── Header.tsx
└── App.tsx

What re-renders and what doesn't?

With useSignal — the component re-renders, not its parents

const name = signal("John")
const age = signal(25)

function NameDisplay() {
  const n = useSignal(name)    // re-renders only when name changes
  return <p>{n}</p>
}

function AgeDisplay() {
  const a = useSignal(age)     // re-renders only when age changes
  return <p>{a}</p>
}

function App() {
  // NEVER re-renders
  return (
    <div>
      <NameDisplay />
      <AgeDisplay />
      <button onClick={() => name.value = "Jane"}>Change name</button>
    </div>
  )
}

// Click "Change name":
//   App         → does NOT re-render
//   NameDisplay → re-renders (reads name)
//   AgeDisplay  → does NOT re-render (reads age, not name)

With <SignalValue> — nothing re-renders except the text

const count = signal(0)
const parity = computed(() => count.value % 2 === 0 ? 'Even' : 'Odd')

function Counter() {
  // This component NEVER re-renders
  return (
    <div>
      <p><SignalValue signal={count} /></p>     {/* only this text updates */}
      <p><SignalValue signal={parity} /></p>    {/* only this text updates */}
      <button onClick={() => count.value++}>+1</button>
    </div>
  )
}

How to verify in the browser

  1. Install the React Developer Tools browser extension
  2. Open DevTools (F12) → go to the Profiler tab
  3. Click the gear icon → check "Highlight updates when components render"
  4. Interact with your app — components that re-render flash with a colored border

When do you still need useEffect?

rxor's effect() replaces most useEffect usage, but not all.

| Situation | Use effect() from rxor | Use useEffect from React | |---|---|---| | React to data changes | Yes | No | | Sync localStorage / document.title | Yes | No | | Log / analytics on change | Yes | No | | Load data when the app starts | Yes (runs immediately) | No | | Load data when a component mounts | No | Yes | | Focus an input on mount | No | Yes | | Set up a timer / interval | No | Yes | | Add event listeners on window | No | Yes |

In practice, rxor eliminates 80-90% of useEffect calls. The remaining ones are for DOM-specific lifecycle operations.


Imports

rxor has three entry points for tree-shaking:

// Core only (zero dependency, works without React)
import { signal, computed, effect, batch, untracked } from 'rxor/core'

// React hooks and components
import { useSignal, useComputed, SignalValue } from 'rxor/react'

// Store
import { createStore, useStore } from 'rxor/store'

// Or import everything from the root
import { signal, computed, useSignal, SignalValue, createStore } from 'rxor'

API reference

Core (rxor/core)

| Export | Description | |---|---| | signal(initial) | Create a reactive signal | | computed(fn) | Create a derived computed value | | effect(fn) | Run a side effect that re-runs on dependency changes | | batch(fn) | Group updates into a single notification | | untracked(fn) | Read values without tracking |

React (rxor/react)

| Export | Description | |---|---| | useSignal(signal) | Subscribe to a signal, component re-renders on change | | useComputed(fn) | Create and subscribe to an inline computed | | <SignalValue signal={sig} /> | Display a signal value without re-rendering the parent |

Store (rxor/store)

| Export | Description | |---|---| | createStore(def) | Group signals, computed, and actions | | useStore(store, selector) | Subscribe to a specific signal in a store |

Types

interface Signal<T> {
  value: T
  peek(): T
  subscribe(cb: (value: T) => void): () => void
}

interface Computed<T> {
  readonly value: T
  peek(): T
  subscribe(cb: (value: T) => void): () => void
}

License

MIT