https://github.com/nfour/xcomponent
A mobx & react microframework
https://github.com/nfour/xcomponent
mobx react typescript
Last synced: over 1 year ago
JSON representation
A mobx & react microframework
- Host: GitHub
- URL: https://github.com/nfour/xcomponent
- Owner: nfour
- Created: 2024-09-09T07:04:21.000Z (almost 2 years ago)
- Default Branch: main
- Last Pushed: 2024-10-18T01:53:03.000Z (almost 2 years ago)
- Last Synced: 2024-10-20T14:11:16.213Z (almost 2 years ago)
- Topics: mobx, react, typescript
- Language: TypeScript
- Homepage:
- Size: 343 KB
- Stars: 3
- Watchers: 1
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# XComponent
A microframework that combines MobX and React to solve common performance, state management, and lifecycle issues.
+ [Install](#install)
+ [Features](#features)
+ [Usage](#usage)
+ [Basic Component](#basic-component)
+ [Inline State](#inline-state)
+ [Lifecycle Hooks](#lifecycle-hooks)
+ [Component Composition](#component-composition)
+ [API](#api)
+ [Core](#core)
+ [Models](#models)
+ [Value](#value)
+ [AsyncValue](#asyncvalue)
+ [BoxedValue](#boxedvalue)
+ [BoolValue](#boolvalue)
+ [Documentation](#documentation)
+ [License](#license)
## Install
```bash
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:
```tsx
// 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:
```tsx
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
```tsx
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}
+
>
)
}
/**
* This demonstrates taking in props, using them observably within X.useState.
*
*
*/
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}
+
>
)
}
```
### 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.
```tsx
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.
```tsx
const Dialog = X(({ children }) => (
{children}
)).with({
Header: X(({ children }) => (
{children}
)),
Body: X(({ children }) => (
{children}
)),
classes: {
dialog: 'dialog',
header: 'dialog-header',
body: 'dialog-body',
}
})
// Usage
Title
Content
```
## API
### Core
- `X()` - 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` - Observable value container
- `AsyncValue` - Async state container with pending/error/value states
- `BoxedValue` - 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
```tsx
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)
```tsx
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.
```tsx
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.
```tsx
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(() =>
<>
Open
...
>
)
```
## Documentation
- [Conventions](./Conventions.md)
- Plug this into your AI instructions as prompts.
## License
MIT