ajwahjs
Framework agnostic state management tools.
Reactive state management library. Manage your application's states, effects, and actions easy way. Make apps more scalable with a unidirectional data-flow.
Every StateController
has the following features:
- Dispatching actions
- Filtering actions
- Adding effects
- Communications among Controllers[
Although they are independents
]
CounterState
interface CounterState {
count: number;
loading: bool;
}
class CounterStateCtrl extends StateController<CounterState> {
constructor() {
super({ count: 0, loading: false });
}
onInit() {}
inc() {
this.emit({ count: this.state.count++ });
}
dec() {
this.emit({ count: this.state.count-- });
}
async asyncInc() {
this.emit({ loading: true });
await delay(1000);
this.emit({ count: this.state.count++, loading: false });
}
asyncIncBy = this.effect<number>((num$) =>
num$.pipe(
tap((_) => this.emit({ loading: true })),
delay(1000),
map((by) => ({ count: this.state.count + by, loading: false }))
)
);
}
Consuming State in
Vanilla js
const csCtrl = Get(CounterStateCtrl);
csCtrl.stream$.subscrie(console.log);
csCtrl.inc();
csCtrl.dec();
csCtrl.asyncInc();
csCtrl.asyncIncBy(5);
React
const CounterComponent = () => {
const csCtrl = Get(CounterStateCtrl);
const data = useStream(csCtrl.stream$, csCtrl.state);
return (
<p>
<button className="btn" onClick={() => csCtrl.inc()}>
+
</button>
<button className="btn" onClick={() => csCtrl.dec()}>
-
</button>
<button className="btn" onClick={() => csCtrl.asyncInc()}>
async(+)
</button>
{data.loading ? 'loading...' : data.count}
</p>
);
};
Angular
@Component({
selector: 'app-counter',
template: `
<p>
<button class="btn" (click)="csCtrl.inc()">+</button>
<button class="btn" (click)="csCtrl.dec()">-</button>
<button class="btn" (click)="csCtrl.asyncIn())">async(+)</button>
<span *ngIf="csCtrl.stream$ | async as state"
>{{ state.loading ? 'loading...' : state.count }}
</span>
</p>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CounterComponent {
constructor(public csCtrl: CounterStateCtrl) {}
}
Vue
<template>
<p>
<button class="btn" @click="inc()">+</button>
<button class="btn" @click="dec()">-</button>
<button class="btn" @click="asyncInc()">async(+)</button>
{{ state.loading?'loading...':state.count }}
</p>
</template>
export default {
name: "Counter",
components: {},
setup() {
const csCtrl = Get(CounterStateCtrl);
const state = useStream(csCtrl.stream$, csCtrl.state);
function inc() {
csCtrl.inc();
}
function dec() {
csCtrl.dec();
}
function asyncInc() {
csCtrl.asyncInc();
}
return { inc, dec, asyncInc, state };
},
};
Effects
onInit() {
this.effectOnAction(
this.action$.isA(AsyncInc).pipe(
tap((_) => this.emit({ loading: true })),
delay(1000),
map((action) => ({ count: this.state.count + action.data, loading: false }))
));
}
asyncIncBy = effect<number>((num$) =>
num$.pipe(
tap((_) => this.emit({ loading: true })),
delay(1000),
tap((by) => this.emit({ count: this.state.count + by, loading: false }))
)
);
Combining States
get todos$() {
return combineLatest([
this.stream$,
this.remoteStream<SearchCategory>(SearchCategoryStateCtrl)
]).pipe(
map(([todos, searchCategory]) => {
switch (searchCategory) {
case SearchCategory.active:
return todos.filter(todo => !todo.completed);
case SearchCategory.completed:
return todos.filter(todo => todo.completed);
default:
return todos;
}
})
);
}
Todo Service
import { Injectable } from "@angular/core";
import { StateController } from './store';
import { getTodos, HasMessage, IAppService, Visibility, SearchTodo, Todo, tween } from './app.service.types'
import { delay, filter, tap, map, combineLatest, startWith, exhaustMap, repeat, takeUntil, endWith } from "rxjs";
@Injectable({ providedIn: 'root' })
export class AppService extends StateController<IAppService>{
constructor() {
super({
message: null,
todos: [],
visibility: 'all',
isSearching: false,
loading: false,
});
}
override onInit() {
this.emit({ todos: getTodos() })
this.effectOnAction(
this.action$.isA(HasMessage).pipe(
filter(_ => this.state.message !== null),
delay(3000),
map(_ => (<IAppService>{ message: null }))
)
)
}
setVisibility(visibility: Visibility) {
this.emit({ visibility })
}
toggleSearch() {
this.emit({ isSearching: !this.state.isSearching })
}
addTodo(task: string) {
if (this.state.isSearching) return
if (!task) {
this.emit({ message: { type: 'error', message: 'Task is required.' } })
return
}
const todos = this.state.todos.concat();
todos.push({ id: todos.length + 1, task, completed: false })
this.throttle({ todos, message: { type: 'info', message: 'Todo added successfully' } });
}
updateTodo(id: number) {
const todos = this.state.todos.map(todo => {
if (todo.id === id) {
todo = { ...todo, completed: !todo.completed }
}
return todo;
});
this.throttle({ todos, message: { type: 'info', message: 'Todo updated successfully' } });
}
removeTodo(id: number) {
const todos = this.state.todos.filter(todo => todo.id !== id);
this.throttle({ todos, message: { type: 'info', message: 'Todo removed successfully' } });
}
#loadingStart$ = this.select(state => state.loading).pipe(filter(val => val));
#loadingEnd$ = this.select(state => state.loading).pipe(filter(val => !val));
rotate$ = this.#loadingStart$.pipe(
exhaustMap(() => tween(0, 360, 1000).pipe(
repeat(),
takeUntil(this.#loadingEnd$),
endWith(0)
))
)
isSearching$ = this.select(state => state.isSearching)
message$ = this.select(state => state.message).pipe(
tap(msg => {
if (msg) { this.dispatch(new HasMessage()) }
}),
);
activeTodo$ = this.select(state => state.todos).pipe(
map(todos => todos.filter(todo => !todo.completed).length));
visibility$ = this.select(state => state.visibility)
todo$ = combineLatest([
this.select(state => state.todos),
this.select(state => state.visibility),
this.action$.isA(SearchTodo).pipe(
filter(_ => this.state.isSearching),
map(search => search.searchText),
startWith('')
)
]).pipe(
map(([todos, visibility, searchText]) => {
if (searchText) {
todos = todos.filter(todo => todo.task.toLowerCase().includes(searchText))
}
if (visibility === 'active') {
todos = todos.filter(todo => !todo.completed)
}
else if (visibility === 'completed') {
todos = todos.filter(todo => todo.completed)
}
return todos;
})
);
throttle = this.effect<Partial<IAppService>>(todo$ => todo$.pipe(
tap(_ => this.emit({ loading: true })),
delay(1300),
map(state => {
state.loading = false;
return state;
})
));
}
counter
: Angular Demo | React Demo | Vue Demo
todos
: Angular Demo | React Demo | Vue Demo