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

@n4s/xcomponent

v5.0.2

Published

A microframework that combines MobX and React to solve common performance, state management, and lifecycle issues.

Downloads

8

Readme

XComponent

A microframework that combines MobX and React to solve common performance, state management, and lifecycle issues.

Install

pnpm add @n4s/xcomponent

Features

  • Drop-in replacement for MobX observer
  • Built-in state management patterns
  • Simplified lifecycle hooks
  • Component composition utilities
  • Helper models for common use cases

Usage

Basic Component

When NOT using a compile plugin to auto-wrap components for observability:

// BEFORE: MobX Observer
import { observer } from 'mobx-react-lite'
const MyComponent = observer((props: { someProp: number }) => <>{props.someProp}</>)

// AFTER: XComponent
import { X } from '@n4s/xcomponent'
const MyComponent = X((props: { someProp: number }) => <>{props.someProp}</>)

If you ARE using a compile plugin to auto-wrap, you can omit the HOC wrapper:

export const MyComponent = (props: { someProp: number }) => {
  const state = X.useState(props, (p) => class {
    foo = new Value(0)

    get computed() {
      return this.foo.value + p.someProp
    }
  })

  return <>{state.computed}</>
}

Inline State

import { X, Value } from '@n4s/xcomponent'

const Counter = () => {
  const state = X.useState(() => class {
    count = new Value(0)
    get doubledCount() {
      return this.count.value * 2
    }

    increment = () => this.count.set(this.count.value + 1)
  })

  return (
    <>
      Count: {state.count.value}, Doubled Count: {state.doubledCount}
      <button onClick={state.increment}>+</button>
    </>
  )
}

/**
 * This demonstrates taking in props, using them observably within X.useState.
 * 
 * <ObservablePropsCounter multiplier={2.5} initialCount={0} />
 */
const ObservablePropsCounter = (props: { initialCount: number, multiplier: number }) => {
  const state = X.useState(props, (props) => class {
    count = new Value(props.initialCount)
    get multipliedCount() {
      return this.count.value * props.multiplier // props.multiplier is observable!
    }

    increment = () => this.count.set(this.count.value + 1)
  })

  return (
    <>
      Count: {state.count.value}, Multiplied Count: {state.multipliedCount}
      <button onClick={state.increment}>+</button>
    </>
  )
}

Lifecycle Hooks

The goal of this library is to avoid using hooks from react during normal state management operations, thus the below lifecycle hooks are provided.

X.useOnMounted(() => {
  // Called when component mounts
})

X.useOnUnmounted(() => {
  // Called when component unmounts
})

X.useReaction(
  () => state.someValue,
  (newValue) => {
    // Called on first render, and whenever observable dependencies change
  }
)

X.useAutorun(() => {
  // Called on first render, and whenever observable dependencies change
})

Component Composition

In the below examples you can see how to create a Dialog component with Header and Body subcomponents.

const Dialog = X(({ children }) => (
  <div className={Dialog.classes.dialog} >{children}</div>
)).with({
  Header: X(({ children }) => (
    <header className={Dialog.classes.header} >{children}</header>
  )),
  Body: X(({ children }) => (
    <div className={Dialog.classes.body}>{children}</div>
  )),
  classes: {
    dialog: 'dialog',
    header: 'dialog-header',
    body: 'dialog-body',
  }
})

// Usage
<Dialog css={{
  // Can also ovveride using the classes we defined.
  [`.${Dialog.classes.dialog}`]: {
    background: 'white',
    padding: '1rem',
  }
}}>
  <Dialog.Header>Title</Dialog.Header>
  <Dialog.Body>Content</Dialog.Body>
</Dialog>

API

Core

  • X<Props>() - Create an observed component with type support
  • X.useState() - Create component-scoped state
  • X.useOnMounted() - Mount lifecycle hook
  • X.useOnUnmounted() - Unmount lifecycle hook
  • X.useReaction() - MobX reaction hook
  • X.useAutorun() - MobX autorun hook

Models

  • Value<T> - Observable value container
  • AsyncValue<T> - Async state container with pending/error/value states
  • BoxedValue<T> - Encapsulated observable with custom getter/setter
  • BoolValue - Boolean value with toggle utilities

Value

The Value class is effectively observable.box of interface { value: T, set: (value: T) => void }.

Features:

  • Type inferrence
  • Async mobx actions (no need to wrap in runInAction or use flow generators)
  • Terseness
  • Avoids reading value until necessary during prop-passing
  • Supports two way binding patterns
const selectedFruit = new Value<'banana'|'apple'|undefined>(undefined)
selectedFruit.set('test') // TS error
selectedFruit.set('banana') // Valid
selectedFruit.value // 'banana'

AsyncValue

Think of react-query for this one. It is a Value that can be in a loading state, and can be awaited.

Features:

  • Ergonomic types
  • Async mobx actions
  • Queuing
  • Promise cancellation
  • Pending state
  • Error state
  • Success state
  • Progress state (eg. for uploads)
async function fetchFiles(c: { userId: string; foo: number }): Promise<{ name: string }[]> {
  return []
}

class ExampleModel {
  constructor() { makeAutoObservable(this) }
  activeUserId = '22'
  files = new AsyncValue(async ({ foo }: { foo: number }) =>
    fetchFiles({ userId: this.activeUserId, foo })
  )
}

const example = new ExampleModel()
example.files.value?.[0]?.name // undefined - missing data
await example.files.query({ foo: 22 }) // foo is strongly typed, inferred!
example.files.value?.[0]?.name // 'myFile.txt' - has data!
example.files.error // undefined - no error
example.files.isPending // false - we already awaited it

const v = new AsyncValue(() => fetchUsersList())
v.value // undefined
const promise = v.query() // Don't need to provide params as none are defined
v.isPending // true
await promise
v.isPending // false
v.value // [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]

BoxedValue

Very similar to Value, however, allows for the getter and setter to be defined seperately, and additionally encapsulates the observable value inside the closure.


const blah = { something: 'banana' }

const somethingFromUri = new BoxedValue(
  // getter
  () => uriRoutes.someRoute.search.something,
  // setter
  (newValue) => uriRoutes.someRoute.push((uri) => ({ search: { something: newValue } })),
)

somethingFromUri.value // 'foo'
somethingFromUri.set('bar')
somethingFromUri.value // 'bar'

// Here we omit the setter, so the value is read-only
// This is effectively just a container encapsulating the value
const somethingWrappedToOptimizeObservability = new BoxedValue(
  () => blah.something,
)

somethingWrappedToOptimizeObservability.value // 'banana'
somethingWrappedToOptimizeObservability.set('banana') // does nothing, because no setter
  

BoolValue

A Value that is specifically for boolean values. It has a few additional methods to make working with booleans easier.


const isOpen = new BoolValue(true)

isOpen.toggle() // false
isOpen.toggle() // true
isOpen.setFalse()
isOpen.value // false
isOpen.isTrue // false
isOpen.setTrue()
isOpen.isTrue // true
isOpen.value // false

const Example = X(() => 
  <>
    <button onClick={isOpen.toggle}>Open</button>
    <Dialog onClose={isOpen.setFalse}>...</Dialog>
  </>
)

Documentation

  • Conventions
    • Plug this into your AI instructions as prompts.

License

MIT