gpui-ts
Version:
A JavaScript framework inspired by gpui and solid-events
1,368 lines (1,078 loc) • 42.4 kB
Markdown
# GPUI-TS
A type-safe reactive state management framework inspired by GPUI and solid-events. GPUI-TS provides centralized model ownership, functional reactive event composition, and declarative rendering with lit-html integration.
## Table of Contents
- [Why GPUI-TS?](#why-gpui-ts)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [API Reference](#api-reference)
- [Comparison with Other Frameworks](#comparison-with-other-frameworks)
- [Common Patterns](#common-patterns)
- [FAQ](#faq)
- [Performance](#performance)
- [Resources](#resources)
## Why GPUI-TS?
Modern web applications struggle with state management complexity. Traditional approaches lead to:
- **State scattered across components** - Hard to track where mutations come from
- **Manual synchronization** - Forgetting to update UI when data changes
- **Type safety gaps** - Runtime errors from incorrect state access
- **Performance issues** - Unnecessary re-renders and memory leaks
GPUI-TS solves these problems with:
```typescript
// Define your entire app state structure with full type inference
const AppSchema = createSchema()
.model('todos', { items: [], filter: 'all' })
.model('ui', { newTodoText: '', editingId: null })
.build()
const app = createApp(AppSchema)
// All state mutations are declarative and type-safe
const [onAddTodo, emitAddTodo] = createEvent<string>()
const validTodo = onAddTodo.filter(text => text.trim().length > 0)
const todos = createSubject(
[],
validTodo(text => todos => [...todos, { text, id: Date.now() }])
)
// Views automatically re-render when state changes
createView(app.models.todos, container, (state, ctx) => html`
<input
.value=${ctx.bind('newTodoText').value}
@input=${ctx.bind('newTodoText').onChange}
/>
<ul>
${state.items.map(todo => html`<li>${todo.text}</li>`)}
</ul>
`)
```
## Quick Start
### Installation
```bash
npm install gpui-ts lit-html
```
### Basic Example
```typescript
import { createSchema, createApp } from 'gpui-ts'
import { createView, html } from 'gpui-ts/lit-html'
// 1. Define your app schema (full type inference)
const CounterSchema = createSchema()
.model('counter', { count: 0 })
.build()
// 2. Create the app
const app = createApp(CounterSchema)
// 3. Create reactive view
createView(app.models.counter, document.body, (state, ctx) => html`
<div>
<h1>Count: ${state.count}</h1>
<button @click=${() => ctx.updateAt('count', c => c + 1)}>
Increment
</button>
</div>
`)
```
### Event-Driven Example
```typescript
// Create events with transformation chains
const [onIncrement, emitIncrement] = createEvent<number>()
const [onReset, emitReset] = createEvent<void>()
// Transform and validate events
const validIncrement = onIncrement.filter(delta => delta > 0)
// Create reactive subjects that respond to events
const counter = createSubject(
0,
validIncrement(delta => count => count + delta),
onReset(() => 0)
)
// Wire up UI
document.getElementById('increment').onclick = () => emitIncrement(1)
document.getElementById('reset').onclick = () => emitReset()
```
## Core Concepts
### Models: Centralized State Ownership
Unlike Redux or Zustand, GPUI-TS models own their state completely. Updates go through a controlled API:
```typescript
// ❌ Direct mutation (impossible in GPUI-TS)
state.user.name = 'John'
// ✅ Controlled updates with context
app.models.user.update((state, ctx) => {
state.name = 'John'
ctx.notify() // Triggers reactive updates
ctx.emit({ type: 'nameChanged', name: 'John' })
})
// ✅ Path-based updates with type safety
app.models.user.updateAt('profile.name', name => name.toUpperCase())
// ✅ Conditional updates with type guards
app.models.user.updateIf(
(state): state is LoggedInUser => state.isLoggedIn,
(user, ctx) => {
// TypeScript knows user.profile exists here
user.profile.lastSeen = new Date()
}
)
// ✅ Convenience methods for common patterns
app.models.counter.updateAndNotify(state => state.count++) // Automatic notification
app.models.ui.set('theme', 'dark') // Direct value assignment
app.models.settings.toggle('notifications') // Boolean toggle
app.models.todos.push('items', newTodo) // Add to array
app.models.todos.removeWhere('items', item => item.completed) // Remove from array
app.models.form.reset() // Reset to initial state
```
### Ergonomic Proxy API: Direct-Style Mutations
For simple state updates, GPUI-TS provides an optional proxy API that feels like direct object manipulation while maintaining all the safety guarantees:
```typescript
// Get a proxy for ergonomic updates
const userProxy = app.models.user.asProxy()
// Direct-style assignments (syntactic sugar over .set())
userProxy.name = 'Jane'
userProxy.profile.age = 31
userProxy.settings.theme = 'dark'
// Works with nested objects and arrays
const todosProxy = app.models.todos.asProxy()
todosProxy.items.push({ id: 3, text: 'New task', completed: false })
todosProxy.items[0].completed = true
// Mix and match with explicit API
userProxy.name = 'Quick Change'
app.models.user.update(state => {
if (state.canBeUpdated) {
state.lastUpdated = new Date()
state.version++
}
})
```
**Key Features:**
- **100% Type-Safe**: Full TypeScript inference for nested properties
- **Backwards Compatible**: Proxy is optional, uses the same update mechanism under the hood
- **Automatic Notifications**: Changes trigger reactive updates just like explicit API calls
- **Cached Proxies**: Same proxy instance returned for the same path for consistency
- **Array Methods**: Native array operations (`push`, `pop`, `splice`, etc.) work seamlessly
- **Batch Updates**: Use `.batch()` to group multiple proxy mutations into a single notification
```typescript
// All of these are equivalent:
userProxy.profile.name = 'Jane'
app.models.user.set('profile.name', 'Jane')
app.models.user.update(state => { state.profile.name = 'Jane' })
// Batch multiple proxy updates for performance
app.models.user.batch(() => {
userProxy.name = 'Jane'
userProxy.age = 30
userProxy.city = 'NYC'
}) // Single notification after all updates complete
```
### Events: Functional Reactive Composition
Events support transformation chains inspired by solid-events:
```typescript
const [onUserInput, emitUserInput] = createEvent<string>()
// Chain transformations with automatic type inference
const validInput = onUserInput
.filter(text => text.length > 0) // Remove empty strings
.map(text => text.trim().toLowerCase()) // Normalize
.debounce(300) // Reduce noise
// Multiple events can feed into subjects
const searchResults = createSubject(
[],
validInput(query => () => searchAPI(query)),
onClearSearch(() => [])
)
```
### Subjects: Reactive State Derivation
Subjects automatically update when their dependencies change:
```typescript
// Subjects respond to multiple event sources
const todoStats = createSubject(
{ total: 0, completed: 0, active: 0 },
onTodoAdded(() => stats => ({
...stats,
total: stats.total + 1,
active: stats.active + 1
})),
onTodoToggled(({ completed }) => stats => ({
...stats,
completed: completed ? stats.completed + 1 : stats.completed - 1,
active: completed ? stats.active - 1 : stats.active + 1
}))
)
```
### Lenses: Composable Data Access
Lenses provide functional, immutable access to nested data structures with full type safety:
```typescript
// Basic lens creation
const nameLens = lens(
(user) => user.name,
(user, name) => ({ ...user, name })
)
// Lens composition
const profileLens = lens(
(user) => user.profile,
(user, profile) => ({ ...user, profile })
)
const nameLens = lens(
(profile) => profile.name,
(profile, name) => ({ ...profile, name })
)
const userNameLens = profileLens.compose(nameLens)
// Using at() for property access
const emailLens = userLens.at('profile').at('email')
// Array operations
const itemsLens = lens(
(state) => state.items,
(state, items) => ({ ...state, items })
)
const firstItemLens = itemsLens.index(0)
const activeItemsLens = itemsLens.filter(item => item.active)
const firstActiveLens = itemsLens.find(item => item.active)
// Advanced operations
const namesLens = itemsLens.map(item => item.name) // Read-only
const hasActiveLens = itemsLens.some(item => item.active) // Read-only
const totalValueLens = itemsLens.reduce((sum, item) => sum + item.value, 0) // Read-only
// Model integration
const userName = app.models.user.lensAt('profile.name')
app.models.user.update((state) => {
const newState = userName.set(state, 'Jane')
Object.assign(state, newState)
})
// Focused models
const profileFocus = app.models.user.focus(profileLens)
profileFocus.update(profile => {
profile.name = 'Jane'
profile.age = 31
})
```
### Model-Scoped Events: Type-Safe Event Handling
GPUI-TS supports both global events and model-scoped events with full type safety:
```typescript
const AppSchema = createSchema()
.model('counter', { count: 0 })
.events({
incremented: (amount: number) => ({ amount }),
reset: () => ({})
})
.model('user', { name: '' })
.events({
login: { payload: { email: string } },
logout: { payload: {} }
})
.build()
const app = createApp(AppSchema)
// Model-scoped events with typed emit/on namespaces
app.models.counter.emit.incremented(5) // Type-safe payload
app.models.counter.on.incremented(amount => {
console.log(`Counter incremented by ${amount}`)
})
// Emit namespace is also callable for ad-hoc events
app.models.counter.emit({ type: 'custom', data: 'value' })
// Global events within update context
app.models.user.update((state, ctx) => {
ctx.emit({ type: 'login', payload: { email: 'user@example.com' } })
})
```
### Memoized Selectors: Efficient Derived State
Create memoized selectors for computed values with automatic dependency tracking and configurable caching:
```typescript
import { createSelector, createModelSelector, shallowEqual } from 'gpui-ts'
// Basic selector with deep equality memoization (default)
const todoStatsSelector = createSelector(
[(state: TodoState) => state.todos, (state: TodoState) => state.filter],
(todos, filter) => ({
total: todos.length,
completed: todos.filter(t => t.completed).length,
active: todos.filter(t => !t.completed).length,
visible: todos.filter(t => {
if (filter === 'completed') return t.completed
if (filter === 'active') return !t.completed
return true
})
})
)
// Shallow equality for better performance with large arrays
const itemCountSelector = createSelector(
[(state) => state.items],
(items) => items.length,
{ equalityFn: shallowEqual }
)
// LRU cache for selectors with varying inputs (e.g., pagination)
const userDataSelector = createSelector(
[(state) => state.currentUserId],
(userId) => expensiveUserComputation(userId),
{
cacheStrategy: 'lru',
maxCacheSize: 10 // Keep last 10 users in cache
}
)
// FIFO cache for time-series or streaming data
const recentEventsSelector = createSelector(
[(state) => state.eventId],
(eventId) => fetchEventData(eventId),
{
cacheStrategy: 'fifo',
maxCacheSize: 20
}
)
// Model-specific selectors
const todoStats = createModelSelector('todos', state => state.items)
// Usage in views
createView(app.models.todos, container, (state, ctx) => {
const stats = todoStatsSelector(app)
return html`
<div>
<p>Total: ${stats.total}</p>
<p>Completed: ${stats.completed}</p>
<p>Active: ${stats.active}</p>
<ul>
${stats.visible.map(todo => html`<li>${todo.text}</li>`)}
</ul>
</div>
`
})
```
**Selector Options:**
- **equalityFn**: Custom equality function (`deepEqual` [default], `shallowEqual`, or custom)
- **cacheStrategy**: `'unbounded'` (default), `'lru'`, or `'fifo'`
- **maxCacheSize**: Maximum cache entries for LRU/FIFO strategies (default: 1)
**When to use each strategy:**
- **Unbounded** (default): Simple selectors with consistent inputs
- **Shallow equality**: Large arrays/objects where reference equality is sufficient
- **LRU cache**: Dynamic inputs (pagination, user selection, search queries)
- **FIFO cache**: Streaming data or time-series where old values become irrelevant
### Event Composition: Advanced Reactive Patterns
GPUI-TS supports sophisticated event composition patterns inspired by solid-events:
```typescript
import { createEvent, createTopic, createPartition, createSubject } from 'gpui-ts'
// Event transformation chains
const [onUserInput, emitUserInput] = createEvent<string>()
const validInput = onUserInput
.filter(text => text.length > 3) // Only process longer inputs
.map(text => text.trim().toLowerCase()) // Normalize
.debounce(300) // Rate limiting
// Event topics - merge multiple sources
const [onMouseMove, emitMouseMove] = createEvent<{x: number, y: number}>()
const [onTouchMove, emitTouchMove] = createEvent<{x: number, y: number}>()
const allMoves = createTopic([onMouseMove, onTouchMove])
// Event partitions - conditional splitting
const [clicks, drags] = createPartition(
allMoves,
event => Math.abs(event.x - startX) > 10 ? 1 : 0 // 1 = drag, 0 = click
)
// Reactive subjects with event reactions
const dragState = createSubject(
{ isDragging: false, startX: 0, startY: 0 },
onMouseDown(({x, y}) => () => ({ isDragging: true, startX: x, startY: y })),
drags(event => state => ({ ...state, currentX: event.x, currentY: event.y })),
onMouseUp(() => () => ({ isDragging: false }))
)
```
### Dynamic Event Management
Add events to running applications at runtime:
```typescript
let app = createApp(createSchema()
.model('user', { name: '' })
.build()
)
// Add events dynamically with full type safety
app = addEvent(app, 'userCreated', {
payload: { id: string, name: string }
})
// The app now has the new event type
app.models.user.update((state, ctx) => {
ctx.emit({ type: 'userCreated', payload: { id: '123', name: state.name } })
})
```
### Schemas: Type-Safe App Definition
Schemas drive complete type inference:
```typescript
const BlogSchema = createSchema()
.model('posts', {
items: [] as Post[],
loading: false,
selectedId: null as string | null
})
.events({
postAdded: (post: Post) => ({ post }),
postDeleted: (id: string) => ({ id })
})
.model('user', {
profile: null as UserProfile | null,
preferences: { theme: 'light', notifications: true }
})
.events({
postSelected: { payload: { id: string } },
themeChanged: { payload: { theme: 'light' | 'dark' } }
})
.plugin(authPlugin) // Add authentication state
.build()
// TypeScript infers everything:
// app.models.posts.emit.postAdded(newPost) // Type-safe event emission
// app.models.posts.on.postAdded(({post}) => console.log(post)) // Type-safe event handling
// app.models.posts.updateAt('items.0.title', title => ...)
// app.models.user.readAt('preferences.theme') // 'light' | 'dark'
```
## Dynamic Schema Management
GPUI-TS supports dynamic schema modifications at runtime and build time, enabling advanced patterns like code-splitting, plugins, and modular architectures.
### Runtime Schema Modification
For applications that need to add or remove features dynamically (e.g., code-splitting, plugins):
```typescript
import { createApp, addModel, removeModel, addEvent } from 'gpui-ts'
let app = createApp(createSchema()
.model('user', { name: '' })
.build()
)
// Add a new model dynamically
app = addModel(app, 'posts', {
initialState: { items: [], loading: false }
})
// The app is now fully typed with the new model
app.models.posts.update(state => {
state.loading = true
})
// Add events dynamically
app = addEvent(app, 'postCreated', {
payload: { title: string }
})
// Remove models when features are unloaded
app = removeModel(app, 'posts')
// TypeScript now knows posts is gone
```
### Build-Time Schema Composition
For composing schemas from multiple modules before app creation:
```typescript
import { createSchema, addModelToSchema, removeModelFromSchema, addEventToSchema } from 'gpui-ts/helpers'
// Feature modules can contribute to schema
export function withAuth(builder) {
let newBuilder = addModelToSchema(builder, 'auth', { user: null })
return addEventToSchema(newBuilder, 'login', { payload: { email: '' } })
}
export function withTodos(builder) {
return addModelToSchema(builder, 'todos', { items: [] })
}
// Compose in main app
let schemaBuilder = createSchema()
.model('ui', { theme: 'dark' })
schemaBuilder = withAuth(schemaBuilder)
schemaBuilder = withTodos(schemaBuilder)
const app = createApp(schemaBuilder.build())
// app.models is fully typed with ui, auth, and todos
```
## API Reference
### Core Functions
#### `createApp<TSchema>(schema: TSchema)`
Creates a GPUI application with full type inference from schema.
```typescript
const app = createApp(MySchema)
// app.models.* are fully typed based on schema
```
#### `createSchema()`
Fluent builder for app schemas:
```typescript
const schema = createSchema()
.model('todos', { items: [] })
.events({ todoAdded: { payload: { text: string } } })
.plugin(uiStatePlugin)
.build()
```
#### `createEvent<T>()`
Creates event handler and emitter with transformation support:
```typescript
const [onEvent, emitEvent] = createEvent<PayloadType>()
// Transform with solid-events style chaining
const transformed = onEvent
.filter(payload => isValid(payload))
.map(payload => normalize(payload))
```
#### `createSubject<T>(initialValue, ...eventHandlers)`
Creates reactive state that responds to events:
```typescript
const count = createSubject(
0,
onIncrement(delta => current => current + delta),
onReset(() => 0)
)
```
#### `addModel<TApp, TModelName, TState>(app, modelName, modelDefinition)`
Dynamically adds a new model to a running GPUI application:
```typescript
let app = createApp(MySchema)
app = addModel(app, 'posts', {
initialState: { items: [], loading: false }
})
// app.models.posts is now available and fully typed
```
#### `removeModel<TApp, TModelName>(app, modelName)`
Removes a model from a running GPUI application and cleans up resources:
```typescript
app = removeModel(app, 'posts')
// app.models.posts is now undefined and TypeScript knows it's gone
```
#### `addEvent<TApp, TEventName, TPayload>(app, eventName, payloadDef)`
Adds a new event definition to the application schema:
```typescript
app = addEvent(app, 'postCreated', {
payload: { title: string, content: string }
})
```
#### `addModelToSchema<TBuilder, TModelName, TState>(builder, modelName, initialState)`
Build-time helper for adding models to schema builders:
```typescript
let builder = createSchema().model('user', { name: '' })
builder = addModelToSchema(builder, 'posts', { items: [] })
```
#### `removeModelFromSchema<TBuilder, TModelName>(builder, modelName)`
Build-time helper for removing models from schema builders:
```typescript
builder = removeModelFromSchema(builder, 'posts')
```
#### `addEventToSchema<TBuilder, TEventName, TPayload>(builder, eventName, payloadDef)`
Build-time helper for adding events to schema builders:
```typescript
builder = addEventToSchema(builder, 'login', { payload: { email: '' } })
```
### Model API
```typescript
interface ModelAPI<T> {
// State access
read(): T
readAt<P extends Path<T>>(path: P): PathValue<T, P>
// Updates
update(updater: (state: T, ctx: ModelContext<T>) => void): this
updateAt<P extends Path<T>>(path: P, updater: (value: PathValue<T, P>) => PathValue<T, P>): this
updateIf<TGuard extends T>(guard: (state: T) => state is TGuard, updater: (state: TGuard, ctx: ModelContext<T>) => void): this
updateAndNotify(updater: (state: T) => void, onError?: (error: unknown, initialState: DeepReadonly<T>) => void): this
// Helper methods for common state manipulations
set<P extends Path<T>>(path: P, value: PathValue<T, P>): this
toggle<P extends Path<T>>(path: PathValue<T, P> extends boolean ? P : never): this
reset(): this
push<P extends Path<T>>(path: P, ...items: PathValue<T, P> extends (infer U)[] ? U[] : never): this
removeWhere<P extends Path<T>>(path: P, predicate: (item: PathValue<T, P> extends (infer U)[] ? U : never) => boolean): this
updateAsync<LoadingKey extends keyof T, ErrorKey extends keyof T>(
updater: (state: T) => Promise<Partial<T>>,
options: {
loadingKey: PathValue<T, LoadingKey> extends boolean ? LoadingKey : never
errorKey: ErrorKey
onError?: (error: unknown, initialState: DeepReadonly<T>) => void
}
): Promise<void>
// Events
emit: {
// Model-scoped typed event emission namespace
// e.g., model.emit.incremented(5) for events defined in schema
// Also callable as function for ad-hoc events: model.emit({ type: 'custom', data })
}
on: {
// Model-scoped typed event subscription namespace
// e.g., model.on.incremented(amount => console.log(amount)) for events defined in schema
}
emitEvent<TEvent>(event: TEvent): this
onEvent<TEvent>(handler: (event: TEvent) => void): () => void
// Subscriptions
onChange(listener: (current: T, previous: T) => void): () => void
subscribeTo<TSource>(source: ModelAPI<TSource>, reaction: (source: TSource, target: T, ctx: ModelContext<T>) => void): ModelSubscription
// Advanced
lens<TFocus>(getter: (state: T) => TFocus): Lens<T, TFocus>
focus<TFocus>(lens: Lens<T, TFocus>): FocusedModel<TFocus, T>
transaction<TResult>(work: (ctx: ModelContext<T>) => TResult): TResult
snapshot(): ModelSnapshot<T>
validate(): ValidationResult<T>
}
```
#### `createSelector<TInput, TResult>(...inputSelectors, combiner, options?)`
Creates a memoized selector function with deep equality checking for optimal performance:
```typescript
// Basic selector with default deep equality
const userDisplayName = createSelector(
[(state: UserState) => state.firstName, (state: UserState) => state.lastName],
(firstName, lastName) => `${firstName} ${lastName}`.trim()
)
const displayName = userDisplayName(userState) // Memoized computation
// With custom options
const cachedSelector = createSelector(
[(state) => state.userId],
(userId) => expensiveComputation(userId),
{
cacheStrategy: 'lru', // or 'fifo' or 'unbounded' (default)
maxCacheSize: 10, // Keep 10 most recent results
equalityFn: shallowEqual // or deepEqual (default) or custom function
}
)
```
#### `createModelSelector<TApp, TModelName, TResult>(model, selector)`
Creates a model-specific selector that automatically provides the model's current state:
```typescript
const userFullName = createModelSelector(app.models.user, userDisplayName)
const name = userFullName() // Automatically uses current user model state
```
#### `createTopic<TEvent>(eventSources)`
Merges multiple event sources into a single event stream:
```typescript
const [onMouseMove, emitMouseMove] = createEvent<{x: number, y: number}>()
const [onTouchMove, emitTouchMove] = createEvent<{x: number, y: number}>()
const allPointerMoves = createTopic([onMouseMove, onTouchMove])
allPointerMoves.subscribe(event => console.log('Pointer moved:', event))
```
#### `createPartition<TEvent>(sourceEvent, partitioner)`
Splits events into multiple streams based on a partitioning function:
```typescript
const [validInputs, invalidInputs] = createPartition(
onUserInput,
input => input.length >= 3 ? 0 : 1 // 0 = valid stream, 1 = invalid stream
)
validInputs.subscribe(input => processValidInput(input))
invalidInputs.subscribe(input => showValidationError(input))
```
### Lit-HTML Integration
```typescript
// Create reactive views
createView(model, container, (state, ctx) => html`
<input .value=${ctx.bind('text').value} @input=${ctx.bind('text').onChange} />
<button @click=${() => ctx.emit(submitEvent({ text: state.text }))}>
Submit
</button>
`)
// Component-style views
const MyComponent = createComponent<{name: string}, {count: number}>((props) => ({
state: createSubject({ count: 0 }),
template: (state, ctx) => html`
<div>${props.name}: ${state.count}</div>
<button @click=${() => ctx.updateAt('count', c => c + 1)}>+</button>
`
}))
```
## Modules Overview
GPUI-TS is organized into several focused modules, each providing specific functionality:
### Core Module (`src/index.ts`)
The foundation of GPUI-TS, providing the core state management engine.
**Key Exports:**
- `createApp()` - Creates the main application instance
- `createSchema()` - Fluent schema builder
- `createEvent()` - Event system with transformation chains
- `createSubject()` - Reactive state containers
- `ModelAPI` - Complete model interface with all state operations
- `ModelRegistry` - Central state management and effect queuing
- `Lens` - Composable data access and updates
- `CRDTManager` - Conflict-free replicated data types support
- `createReducer()` - Reducer-based state management
- Dynamic schema modification functions (`addModel`, `removeModel`, `addEvent`)
**Features:**
- Centralized model ownership with queued effects
- Functional reactive event composition
- Advanced type inference and path manipulation
- Transaction support with rollback
- Time travel debugging
- Comprehensive validation
### Lit-HTML Integration (`src/lit.ts`)
Seamless integration with lit-html for reactive rendering.
**Key Exports:**
- `createView()` - Reactive view binding to models
- `createComponent()` - Component-style view composition
- `bind()` - Form input binding directive
- `when()` - Conditional rendering directive
- `forEach()` - List rendering directive
- `asyncTemplate()` - Async operation rendering
- `suspense()` - Loading/error/success state rendering
- `devView()` - Development mode debugging
- `performanceView()` - Performance monitoring
**Features:**
- Automatic re-rendering on state changes
- Type-safe template functions
- Automatic cleanup and lifecycle management
- Performance optimized rendering
- Development mode debugging
### Schema Helpers (`src/helpers.ts`)
Type-safe utilities for building and manipulating schemas.
**Key Exports:**
- `createSchema()` - Fluent schema builder
- `createModelSchema()` - Advanced model schema configuration
- `mergeSchemas()` - Schema composition
- `validators` - Built-in validation rules
- `combineValidators()` - Validation composition
- `validateSchema()` - Schema validation
- `introspectSchema()` - Schema analysis
- `generateTypes()` - TypeScript type generation
- Standalone composition helpers (`addModelToSchema`, etc.)
**Features:**
- Fluent API for schema definition
- Schema composition and merging
- Type-safe model extensions
- Validation and constraints helpers
- Plugin system for schema augmentation
- Development utilities
### Advanced Features (`src/advanced.ts`)
Powerful patterns for complex state management scenarios.
**Key Exports:**
- `createReactiveView()` - Fine-grained reactivity with signals
- `createResource()` - Formalized async state management
- `createMachineModel()` - XState integration
- `Signal` - Reactive primitive for fine-grained updates
- `Computed` - Derived reactive values
**Features:**
- Signal-based reactivity for optimal performance
- Declarative async data fetching with loading states
- State machine integration with XState
- Automatic race condition handling
- Fine-grained DOM updates
### Ergonomic Context API (`src/ergonomic.ts`)
Composition API-style interface using unctx for cleaner setup code.
**Key Exports:**
- `createAppWithContext()` - Context-aware app creation
- `useApp()` - Access active application instance
- `useModel()` - Direct model access by name
- `useResource()` - Context-aware resource creation
- `useMachineModel()` - Context-aware state machine integration
- `useSignalFromModel()` - Bridge models to signals
**Features:**
- Global context management with unctx
- Ergonomic hooks for common operations
- Type-safe model access without prop drilling
- Cleaner, more modular setup code
- Async-safe context usage patterns
### Additional Modules
**Signals (`src/signals.ts`):**
- Reactive primitives for fine-grained reactivity
- Integration with GPUI-TS models
- Signal-based view updates
**Resources (`src/resource.ts`):**
- Specialized async state management
- Loading, error, and success state handling
- Automatic dependency tracking
**Infinite Resources (`src/infinite-resource.ts`):**
- Pagination and infinite scrolling support
- Virtual scrolling integration
- Memory-efficient large dataset handling
**CRDT (`src/crdt.ts`):**
- Conflict-free replicated data types
- Collaborative editing support
- Operation broadcasting and conflict resolution
**Robot (`src/robot.ts`):**
- State machine and robot pattern implementations
- Complex workflow management
- Hierarchical state handling
Each module is designed to be used independently or in combination, providing a flexible and scalable architecture for building complex applications with type safety and excellent developer experience.
## Comparison with Other Frameworks
### vs Redux/RTK
| Feature | GPUI-TS | Redux/RTK |
|---------|---------|-----------|
| **Boilerplate** | Minimal with schema inference | High (actions, reducers, selectors) |
| **Type Safety** | Complete compile-time safety | Requires manual typing |
| **Learning Curve** | Moderate (new concepts) | High (many concepts) |
| **Performance** | Automatic batching, fine-grained updates | Requires React.memo optimization |
| **Side Effects** | Built-in effect system | Requires middleware (thunks, sagas) |
```typescript
// Redux: Multiple files, lots of boilerplate
const ADD_TODO = 'ADD_TODO'
interface AddTodoAction { type: typeof ADD_TODO; payload: { text: string } }
const addTodo = (text: string): AddTodoAction => ({ type: ADD_TODO, payload: { text } })
const todosReducer = (state = [], action: AnyAction) => { /* ... */ }
// GPUI-TS: Single declaration, full type inference
const TodoSchema = createSchema()
.model('todos', { items: [] as Todo[] })
.build()
```
### vs Zustand
| Feature | GPUI-TS | Zustand |
|---------|---------|---------|
| **State Updates** | Centralized with controlled mutations | Direct mutations in stores |
| **Reactivity** | Automatic reactive subscriptions | Manual selector-based subscriptions |
| **Event System** | First-class events with composition | No built-in event system |
| **Validation** | Built-in schema validation | Manual validation |
```typescript
// Zustand: Manual subscriptions
const useTodoStore = create((set) => ({
todos: [],
addTodo: (text) => set((state) => ({ todos: [...state.todos, { text }] }))
}))
// GPUI-TS: Reactive subjects
const todos = createSubject([], onTodoAdded(text => todos => [...todos, { text }]))
```
### vs MobX
| Feature | GPUI-TS | MobX |
|---------|---------|------|
| **Predictability** | Explicit updates through controlled API | Implicit updates via proxies |
| **Debugging** | Clear update paths, time travel | Can be hard to track mutation sources |
| **Type Safety** | Full TypeScript integration | Good but requires decorators/setup |
| **Framework Coupling** | Framework agnostic | Tight React coupling |
### vs React Hooks
| Feature | GPUI-TS | React Hooks |
|---------|---------|-------------|
| **State Sharing** | Global reactive models | Props drilling or Context |
| **Derived State** | Automatic with subjects | Manual with useMemo |
| **Side Effects** | Built-in effect system | useEffect dependencies |
| **Testing** | Framework-independent models | Component testing complexity |
```typescript
// React: Complex dependency management
const [todos, setTodos] = useState([])
const [filter, setFilter] = useState('all')
const filteredTodos = useMemo(() =>
todos.filter(todo => filter === 'all' || todo.status === filter),
[todos, filter]
)
// GPUI-TS: Automatic reactive derivation
const filteredTodos = createSubject(
[],
onTodosChanged(todos => () => filterTodos(todos, currentFilter())),
onFilterChanged(filter => () => filterTodos(currentTodos(), filter))
)
```
## Common Patterns
### Form Handling
```typescript
const FormSchema = createSchema()
.model('form', {
values: { name: '', email: '', age: 0 },
errors: {} as Record<string, string>,
touched: {} as Record<string, boolean>,
submitting: false
})
.build()
const app = createApp(FormSchema)
// Validation
const validateField = (field: string, value: any) => {
if (field === 'email' && !value.includes('@')) {
return 'Invalid email'
}
return null
}
// Form view with automatic validation
createView(app.models.form, container, (state, ctx) => html`
<form @submit=${(e: Event) => {
e.preventDefault()
ctx.update(state => { state.submitting = true })
submitForm(state.values)
}}>
<input
name="email"
.value=${ctx.bind('values.email').value}
@input=${ctx.bind('values.email').onChange}
@blur=${() => {
const error = validateField('email', state.values.email)
ctx.updateAt('errors.email', () => error)
ctx.updateAt('touched.email', () => true)
}}
/>
${state.errors.email && state.touched.email ?
html`<div class="error">${state.errors.email}</div>` : ''
}
<button type="submit" ?disabled=${state.submitting}>
${state.submitting ? 'Submitting...' : 'Submit'}
</button>
</form>
`)
```
### Async Data Loading
```typescript
const [onLoadUser, emitLoadUser] = createEvent<{ id: string }>()
const [onUserLoaded, emitUserLoaded] = createEvent<User>()
const [onUserError, emitUserError] = createEvent<Error>()
const userState = createSubject(
{ data: null, loading: false, error: null },
onLoadUser(() => () => ({ data: null, loading: true, error: null })),
onUserLoaded(user => () => ({ data: user, loading: false, error: null })),
onUserError(error => () => ({ data: null, loading: false, error }))
)
// Side effect for API calls
onLoadUser.subscribe(async ({ id }) => {
try {
const user = await fetchUser(id)
emitUserLoaded(user)
} catch (error) {
emitUserError(error)
}
})
// View with suspense-like behavior
createView(userModel, container, (state, ctx) => html`
${suspense(state, {
loading: html`<div class="spinner">Loading...</div>`,
error: (error) => html`<div class="error">Error: ${error.message}</div>`,
success: (user) => html`<div>Welcome, ${user.name}!</div>`
})}
`)
```
### Real-time Updates
```typescript
const [onSocketMessage, emitSocketMessage] = createEvent<SocketMessage>()
// Different message types
const [userMessages, systemMessages, errorMessages] = createPartition(
onSocketMessage,
msg => msg.type === 'user' ? 0 : msg.type === 'system' ? 1 : 2
)
const chatState = createSubject(
{ messages: [], users: [], errors: [] },
userMessages(msg => state => ({
...state,
messages: [...state.messages, msg]
})),
systemMessages(msg => state => ({
...state,
users: msg.type === 'user_joined' ? [...state.users, msg.user] : state.users
})),
errorMessages(msg => state => ({
...state,
errors: [...state.errors, msg.error]
}))
)
// WebSocket integration
const socket = new WebSocket('ws://localhost:8080')
socket.onmessage = (event) => {
emitSocketMessage(JSON.parse(event.data))
}
```
## FAQ
### Q: How does GPUI-TS compare to React's built-in state management?
GPUI-TS provides centralized, reactive state that can be shared across your entire application without prop drilling or complex Context setups. React's useState is component-local, while GPUI-TS models are global and reactive.
### Q: Can I use GPUI-TS with React?
Yes! GPUI-TS is framework-agnostic. You can subscribe to model changes in React components:
```typescript
function MyReactComponent() {
const [state, setState] = useState(todoModel.read())
useEffect(() => {
return todoModel.onChange(newState => setState(newState))
}, [])
return <div>{state.items.length} todos</div>
}
```
### Q: How does performance compare to other state management solutions?
GPUI-TS uses automatic batching and fine-grained reactivity. Updates are queued and flushed synchronously, preventing cascading re-renders. The lit-html integration only updates changed DOM nodes.
### Q: What's the learning curve like?
If you're familiar with Redux, the concepts translate well but with less boilerplate. If you know RxJS, the event composition patterns will feel natural. The hardest part is typically understanding centralized model ownership vs. local component state.
### Q: How do I handle side effects?
GPUI-TS has built-in effect systems:
```typescript
model.update((state, ctx) => {
state.data = newData
ctx.effect((currentState, cleanup) => {
const timer = setInterval(() => console.log(currentState), 1000)
cleanup(() => clearInterval(timer))
})
})
```
### Q: Can I gradually adopt GPUI-TS in an existing app?
Yes! Start by converting a single piece of global state to a GPUI-TS model. The framework is designed for gradual adoption.
### Q: How do I debug state changes?
GPUI-TS includes development tools:
```typescript
// Enable debug mode
enableDevMode(app)
// Access debug info in browser console
window.__GPUI_DEBUG__.logAllState()
window.__GPUI_DEBUG__.analyzePerformance()
// Time travel debugging
const snapshot = model.snapshot()
// ... make changes ...
model.restore(snapshot)
```
### Q: What about TypeScript support?
GPUI-TS is built with TypeScript-first design. Schema definitions drive complete type inference throughout your app, eliminating the need for manual type annotations in most cases.
## Performance
<!-- ### Benchmarks
Based on TodoMVC implementations:
| Framework | Bundle Size | Memory Usage | Update Performance |
|-----------|-------------|--------------|-------------------|
| GPUI-TS | 12kb gzipped | Low (centralized state) | Excellent (batched updates) |
| Redux + RTK | 15kb gzipped | Medium (normalized state) | Good (with React.memo) |
| Zustand | 8kb gzipped | Low | Good |
| MobX | 16kb gzipped | Medium (proxy overhead) | Excellent | -->
### Built-in Performance Features
GPUI-TS includes several built-in optimizations that work automatically:
**1. Configurable Selector Memoization**
- **Deep equality** (default): Safe for all use cases, recomputes only when values change
- **Shallow equality**: 50x faster for large arrays, use when reference equality is sufficient
- **Custom equality**: Define your own comparison logic for specific needs
```typescript
// Default: deep equality (safe, comprehensive)
const selector1 = createSelector([selectItems], items => items.filter(...))
// Opt-in: shallow equality (fast for large arrays)
const selector2 = createSelector(
[selectItems],
items => items.length,
{ equalityFn: shallowEqual }
)
```
**2. Bounded Cache Strategies**
- **Unbounded** (default): Best for stable inputs, unlimited cache
- **LRU cache**: Best for pagination, user switching, search - keeps N most recently used
- **FIFO cache**: Best for streaming/time-series - keeps N most recently added
```typescript
// LRU cache prevents memory growth with dynamic inputs
const userSelector = createSelector(
[selectUserId],
userId => fetchUserData(userId),
{ cacheStrategy: 'lru', maxCacheSize: 10 }
)
```
**3. Proxy Batching**
- Group multiple proxy mutations into a single notification
- Reduces re-renders from N to 1 for N updates
```typescript
// Without batching: 3 separate notifications
userProxy.name = 'Jane'
userProxy.age = 30
userProxy.city = 'NYC'
// With batching: 1 notification
app.models.user.batch(() => {
userProxy.name = 'Jane'
userProxy.age = 30
userProxy.city = 'NYC'
})
```
### Optimization Tips
1. **Use batch operations** for multiple updates:
```typescript
app.batch(() => {
model1.update(...)
model2.update(...)
model3.update(...)
}) // Single re-render
```
2. **Choose the right selector strategy**:
```typescript
// Simple selectors: unbounded cache (default)
const selectCount = createSelector([selectItems], items => items.length)
// Dynamic inputs (pagination): LRU cache
const selectPage = createSelector(
[selectPageNum],
page => fetchPage(page),
{ cacheStrategy: 'lru', maxCacheSize: 5 }
)
// Large arrays: shallow equality
const selectIds = createSelector(
[selectItems],
items => items.map(i => i.id),
{ equalityFn: shallowEqual }
)
```
3. **Use path-based updates** for deep objects:
```typescript
// ✅ Efficient: only updates specific path
model.updateAt('user.profile.settings.theme', theme => theme === 'dark' ? 'light' : 'dark')
// ❌ Inefficient: updates entire state
model.update(state => {
state.user.profile.settings.theme = state.user.profile.settings.theme === 'dark' ? 'light' : 'dark'
})
```
### Performance Benchmarks
**Selector Memoization:**
- Deep equality with 10,000 items: ~5ms per comparison
- Shallow equality with 10,000 items: ~0.1ms per comparison (50x faster)
- LRU cache memory: O(maxCacheSize) vs O(∞) for unbounded
**Batching:**
- 10 proxy updates without batching: ~100ms (10 re-renders)
- 10 proxy updates with batching: ~10ms (1 re-render, 10x faster)
## Resources
<!-- ### Documentation
- [API Reference](./docs/api.md)
- [Schema Guide](./docs/schemas.md)
- [Event System Guide](./docs/events.md)
- [Lit-HTML Integration](./docs/lit-html.md)
- [Migration Guide](./docs/migration.md)
### Examples
- [TodoMVC Implementation](./examples/todomvc)
- [Real-time Chat App](./examples/chat)
- [E-commerce Dashboard](./examples/dashboard)
- [Form Validation](./examples/forms)
### Community
- [GitHub Discussions](https://github.com/gpui-ts/gpui-ts/discussions)
- [Discord Server](https://discord.gg/gpui-ts)
- [Stack Overflow Tag](https://stackoverflow.com/questions/tagged/gpui-ts) -->
### Related Projects
- [lit-html](https://lit.dev/docs/libraries/lit-html/) - Template library
- [solid-events](https://github.com/devagrawal09/solid-events) - Event composition inspiration
### Contributing
- [Contributing Guide](./CONTRIBUTING.md)
- [Code of Conduct](./CODE_OF_CONDUCT.md)
- [Architecture Decision Records](./docs/adr/)
---
**GPUI-TS** - Reactive state management that scales from simple counters to complex applications.
[Get Started](./docs/getting-started.md) | [API Docs](./docs/api.md) | [Examples](./examples/) | [GitHub](https://github.com/gpui-ts/gpui-ts)