A microframework that combines MobX and React to solve common performance, state management, and lifecycle issues.
pnpm add @n4s/xcomponent
- Drop-in replacement for MobX
observer
- Built-in state management patterns
- Simplified lifecycle hooks
- Component composition utilities
- Helper models for common use cases
import { X } from '@n4s/xcomponent'
const MyComponent = X<{ count: number }>(({ count }) => (
<div>{count}</div>
))
import { X, Value } from '@n4s/xcomponent'
const Counter = X(() => {
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 = X<{ initialCount: number, multiplier: number }>((props) => {
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>
</>
)
})
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
})
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>
X<Props>()
- Create an observed component with type supportX.useState()
- Create component-scoped stateX.useOnMounted()
- Mount lifecycle hookX.useOnUnmounted()
- Unmount lifecycle hookX.useReaction()
- MobX reaction hookX.useAutorun()
- MobX autorun hook
Value<T>
- Observable value containerAsyncValue<T>
- Async state container with pending/error/value statesBoxedValue<T>
- Encapsulated observable with custom getter/setterBoolValue
- Boolean value with toggle utilities
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 useflow
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'
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' }]
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
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>
</>
)
- Extended Documentation and Examples
- Further reading for advanced use cases, justifications etc.
- Conventions
- Plug this into your AI instructions as prompts.
MIT