Skip to content

nfour/xcomponent

Repository files navigation

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

For day-to-day usage, treat SKILL.md as the primary guide. It is the first-class usage reference for authoring xcomponent-based components, including build-time auto observer wrapping, local state patterns, reactions, async state, and composition.

Use this README for the compact overview. Use SKILL.md for the authoritative workflow, caveats, and expanded examples.

Basic Component

Manual wrap mode:

import { X } from '@n4s/xcomponent'

export const UserName = X(({ name }: { name: string }) => {
  return <span>{name}</span>
})

Build-time auto-wrap mode:

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

export const UserName = (props: { name: string }) => {
  const state = X.useState(props, (props) => class {
    prefix = new Value('User:')

    get label() {
      return `${this.prefix.value} ${props.name}`
    }
  })

  return <span>{state.label}</span>
}

Use bare functions only when your build step really does inject MobX observation.

Inline State

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

export const Counter = X(() => {
  const state = X.useState(() => class {
    count = new Value(0)

    get doubled() {
      return this.count.value * 2
    }

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

  return (
    <>
      <div>{state.count.value}</div>
      <div>{state.doubled}</div>
      <button onClick={state.increment}>+</button>
    </>
  )
})

For observable props, custom-hook extraction, and functional-state variants, use SKILL.md.

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
})

X.useReaction uses fireImmediately: true and structural comparison by default. See SKILL.md for the caveats around observable tracking and prop-driven reactions.

Component Composition

Use .with() to attach subcomponents or static class maps.

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

See SKILL.md for the full composition guidance and usage patterns.

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
  • Value.Async<T> - Async state container with pending/error/value states
  • Value.Boxed<T> - Encapsulated observable with custom getter/setter
  • Value.Bool - Boolean value with toggle utilities

For workflow guidance on when to choose each model, use SKILL.md.

Value

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

const selectedFruit = new Value<'banana'|'apple'|undefined>(undefined)
selectedFruit.set('test') // TS error
selectedFruit.set('banana') // Valid
selectedFruit.value // 'banana'

Value.Async

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

const users = new Value.Async(async ({ orgId }: { orgId: string }) => {
  return fetchUsers(orgId)
})

await users.query({ orgId: 'acme' })
users.value
users.error
users.isPending

For query lifecycle details such as reset(), clone(), and prop-driven querying, use SKILL.md.

Value.Boxed

Very similar to Value, however, it allows the getter and setter to be defined separately and encapsulates the observable value inside the closure.

const selectedId = new Value.Boxed(
  () => route.search.userId,
  (userId) => route.push((uri) => ({ search: { userId } })),
)

Read boxed.value inside render or a computed getter so MobX can track it. See SKILL.md for the reactivity caveat.

Value.Bool

Use for booleans with convenience helpers.

const isOpen = new Value.Bool(false)
isOpen.toggle()
isOpen.setTrue()
isOpen.setFalse()

Documentation

  • SKILL.md
    • First-class usage guide for humans and AI tooling.
  • src/stories/demoApp
    • Larger example app structure using xcomponent patterns.

If README and SKILL.md ever disagree, prefer SKILL.md.

License

MIT

About

A mobx & react microframework

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

No releases published

Contributors