@mmuscat/angular-actions
v0.1200.0-next.4
Published
A tiny (1kb) state management library for Angular Composition API.
Downloads
7
Maintainers
Readme
Angular Actions
A tiny (1kb) state management library for Angular Composition API.
Quick Start
npm install @mmuscat/angular-actions
yarn add @mmuscat/angular-actions
Create a store
Define initial state factory. Supports dependency injection.
export function getInitialState() {
return {
count: 0,
}
}
Actions
Create actions, with or without props.
const Increment = new Action("Increment")
const IncrementBy = new Action("Increment", props<{ by: number }>())
Reducers
Create reducers. A list of action reducers can be added for producing the next state. Supports dependency injection.
const Count = new Reducer<number>("count", (reducer) =>
reducer.add(Increment, (state, action) => state + 1),
)
Effects
Create effects. Effects are factory functions that should return an Observable
. Supports
dependency injection. Effects will receive the store injector as an argument.
function logCount(store: Store) {
const count = store(Count)
return count.pipe(tap(console.log))
}
function autoIncrement(store: Store) {
const increment = store(Increment)
return interval(1000).pipe(tap(increment))
}
Error Recovery
Effects will stop running by default when an error occurs. To prevent this from happening,
handle the error using catchError
or another retry strategy. If you just want errors to be
reported while keeping an effect running, return a materialized stream from an error-producing
inner observable.
function effectWithErrors(store: Store) {
const http = inject(HttpClient)
const source = store(Count)
const result = store(ResultAction)
return source.pipe(
switchMap((count) =>
http.post("url", { count }).pipe(
map(result),
materialize(), // should be placed on an inner stream
),
),
)
}
Module Store
Create a Store.
const AppStore = new Store("AppStore", {
state: getInitialState,
reducers: [Count],
effects: [logCount, autoIncrement],
})
Provide the store to root module. Additional stores can be configured for each lazy loaded module. Effects will run immediately on bootstrap.
@NgModule({
imports: [StoreModule.config(AppStore)],
})
export class AppModule {}
Component Store
Create a Store.
const MyStore = new Store("MyStore", {
state: getInitialState,
reducers: [Count],
effects: [logCount, autoIncrement],
})
Provide and use the store in a component. Effects won't run until the store is injected.
function setup() {
const store = inject(MyStore)
const count = store(Count) // get value from store
const increment = store(Increment) // get dispatcher from store
return {
count,
increment,
}
}
@Component({
providers: [MyStore.Provider],
})
export class MyComponent extends ViewDef(setup) {}
Important: A note about dependency injection
You must use the store's injector to retrieve actions and values.
const count = inject(Count) // don't do this!
const store = inject(MyStore) // inject store first
const count = store(Count) // this will be the correct instance
API Reference
Action
Creates an injectable Emitter
that will emit actions of a given kind
, with or without data.
const Increment = new Action("Increment")
const SaveTodo = new Action("SaveTodo", props<Todo>())
Actions can be injected inside the setup function of a ViewDef
or Service
factory. This
returns an Emitter
that be used to dispatch or listen to events.
function setup() {
const store = inject(MyStore)
const increment = store(Increment)
subscribe(increment, ({ kind }) => {
console.log(kind) // "Increment"
})
setTimeout(increment, 1000)
return {
increment,
}
}
@Component()
export class MyComponent extends ViewDef(setup) {}
Reducer
Creates an injectable Value
that reduces actions to produce a new state. The state of the
reducer is hydrated using the object key of the same name returned by getInitialState
.
function getInitialState() {
return {
count: 0, // state key must match reducer name
}
}
const Count = new Reducer(
"count", // reducer name must match state key
(reducer) => reducer.add(Increment, (state, action) => state + 1),
// .add(OtherAction, (state, action) => etc)
)
You can also supply a list of actions to a single reducer.
const Increment = new Action("Increment", props<{ by: number }>())
const Add = new Action("Add", props<{ by: number }>())
const Count = new Reducer(count, (reducer) =>
reducer.add([Increment, Add], (state, action) => state + action.by),
)
Reducers can be injected inside the setup function of a ViewDef
or Service
factory. This
returns a Value
that be used to get, set or observe state changes.
function setup() {
const store = inject(MyStore)
const count = store(Count)
subscribe(() => {
console.log(count()) // 0
})
return {
count,
}
}
@Component()
export class MyComponent extends ViewDef(setup) {}
props
Returns a typed function for producing data
on an Action
.
const Increment = new Action("Increment", props<{ by: number }>())
Which is equivalent to
const Increment = new Action("Increment", (data: { by: number }) => data)