https://github.com/irvrodflo/ngx-state-crafter
Simple and predictable state management for Angular 17+. No actions, no reducers — just signals.
https://github.com/irvrodflo/ngx-state-crafter
Last synced: 5 months ago
JSON representation
Simple and predictable state management for Angular 17+. No actions, no reducers — just signals.
- Host: GitHub
- URL: https://github.com/irvrodflo/ngx-state-crafter
- Owner: irvrodflo
- License: mit
- Created: 2026-02-22T02:28:10.000Z (5 months ago)
- Default Branch: main
- Last Pushed: 2026-02-22T19:22:48.000Z (5 months ago)
- Last Synced: 2026-02-23T00:07:33.487Z (5 months ago)
- Language: TypeScript
- Size: 78.1 KB
- Stars: 2
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- awesome-angular - ngx-state-crafter - A lightweight, signal‑driven state library for Angular with a clean, boilerplate‑free API. (State Management / Other State Libraries)
- fucking-awesome-angular - ngx-state-crafter - A lightweight, signal‑driven state library for Angular with a clean, boilerplate‑free API. (State Management / Other State Libraries)
README
# ngx-state-crafter
A lightweight, reactive state management library for Angular, built on top of Angular Signals.
**ngx-state-crafter** gives you structure without boilerplate. No actions, no reducers, no decorators — just a simple API that scales from a single component to complex feature states.
---
## Requirements
- Angular 17+
---
## Installation
```bash
npm install @irv-labs/ngx-state-crafter
```
---
## Quick Start
```typescript
import { craftState } from '@irv-labs/ngx-state-crafter';
interface CounterState {
count: number;
label: string;
}
const state = craftState({
count: 0,
label: 'My Counter',
});
// Read values as signals
console.log(state.count()); // 0
// Update state
state.update({ count: 1 });
// Update with a function
state.update({ count: (prev) => prev + 1 });
```
---
## API
### `craftState(initial: T): StateInstance`
Creates a new reactive state instance from an initial object.
```typescript
const state = craftState({ count: 0, name: 'Guest' });
```
---
### `.update(updater)`
Updates one or more properties. Accepts a partial object or a function that receives the current state.
```typescript
// Direct value
state.update({ count: 10 });
// Function updater
state.update({ count: (prev) => prev + 1 });
// Multiple properties at once
state.update({ count: 0, name: 'Alice' });
// Function that returns partial state
state.update((s) => ({ count: s.count + 1 }));
```
---
### `.select(key)`
Returns the signal for a specific property.
```typescript
const count = state.select('count'); // Signal
count(); // read current value
```
---
### `.computed(projector)`
Creates a derived signal from the state. Automatically updates when dependencies change.
```typescript
const doubled = state.computed((s) => s.count * 2);
const label = state.computed((s) => `${s.name}: ${s.count}`);
```
---
### `.effect(fn)`
Runs a side effect whenever the state changes. Returns an `EffectRef`.
```typescript
const ref = state.effect((s) => {
console.log('State changed, count is:', s.count);
});
// Cleanup when done
ref.destroy();
```
---
### `.watch(key, fn)`
Watches a single property and runs a callback when it changes. Returns an `EffectRef`.
```typescript
const ref = state.watch('count', (value) => {
console.log('Count is now:', value);
});
```
---
### `.when(predicate, fn)`
Runs a callback when a condition becomes `true`. Returns an `EffectRef`.
```typescript
const ref = state.when(
(s) => s.count > 10,
() => console.log('Count exceeded 10!'),
);
```
---
### `.merge(key, partial)`
Partially updates a nested object property without replacing the entire value.
```typescript
interface AppState {
user: { name: string; role: string };
}
const state = craftState({
user: { name: 'Guest', role: 'viewer' },
});
// Only updates role, keeps name intact
state.merge('user', { role: 'admin' });
```
---
### `.snapshot()`
Returns the current state as a plain, non-reactive object.
```typescript
const snap = state.snapshot();
console.log(snap); // { count: 5, name: 'Alice' }
```
---
### `.reset()`
Resets the state back to its initial values.
```typescript
state.reset();
```
---
### `.debug(key, label?)`
Logs a property's value to the console whenever it changes. Meant for development only.
```typescript
state.debug('count', 'MyComponent');
// [State Debug: MyComponent] count -> 5
```
---
## Usage in a Component
```typescript
import { Component } from '@angular/core';
import { craftState } from '@irv-labs/ngx-state-crafter';
interface FormState {
email: string;
submitted: boolean;
}
@Component({
selector: 'app-form',
template: `
Submit
`,
})
export class FormComponent {
state = craftState({ email: '', submitted: false });
email = this.state.select('email');
isValid = this.state.computed((s) => s.email.includes('@'));
constructor() {
this.state.when(
(s) => s.submitted,
() => console.log('Form submitted!'),
);
}
setEmail(value: string) {
this.state.update({ email: value });
}
submit() {
this.state.update({ submitted: true });
}
}
```
---
## Usage in a Service
`craftState` works great inside Angular services for shared or feature-level state.
```typescript
import { Injectable } from '@angular/core';
import { craftState } from '@irv-labs/ngx-state-crafter';
interface CartState {
items: { id: number; name: string; price: number }[];
discount: number;
}
@Injectable({ providedIn: 'root' })
export class CartService {
private state = craftState({ items: [], discount: 0 });
items = this.state.select('items');
total = this.state.computed((s) => s.items.reduce((sum, i) => sum + i.price, 0) - s.discount);
addItem(item: { id: number; name: string; price: number }) {
this.state.update({ items: (prev) => [...prev, item] });
}
applyDiscount(amount: number) {
this.state.update({ discount: amount });
}
reset() {
this.state.reset();
}
}
```
---
## TypeScript Support
`ngx-state-crafter` is fully typed. All methods infer types from your state interface automatically.
```typescript
interface MyState {
count: number;
name: string;
}
const state = craftState({ count: 0, name: '' });
state.update({ count: 'oops' }); // Type error
state.select('unknown'); // Type error
state.merge('count', {}); // Type error — count is not an object
```
---
## Safety
All callbacks passed to `watch`, `when`, and `effect` are internally wrapped with `untracked`. This means you can safely read or write other signals inside these callbacks without risking infinite reactive loops.
---
## Path Alias (optional)
If you prefer a shorter import, configure a path alias in your `tsconfig.json`:
```json
{
"compilerOptions": {
"paths": {
"@state-crafter": ["./node_modules/@irv-labs/ngx-state-crafter"]
}
}
}
```
Then import from the alias instead:
```typescript
// Before
import { craftState } from '@irv-labs/ngx-state-crafter';
// After
import { craftState } from '@state-crafter';
```
> This is purely a local convenience — it does not affect the published package or your teammates unless they add the same alias to their `tsconfig.json`.
---
## Philosophy
- **No boilerplate** — one function call to get a fully reactive state
- **Signal-native** — built entirely on Angular Signals, no RxJS required
- **Predictable** — safe by default, no surprise loops
- **Minimal API** — learn it in 10 minutes, use it anywhere
---
## License
MIT