How to manage state in Angular - the reactive way
Philipp Kief | 2023
Reactive State
export class AppComponent {
todos: Todo[] = [];
addTodo(description: string) {
this.todos.push({ completed: false, description });
}
setTodoCompleteState(todo: Todo, completed: boolean) {
this.todos = this.todos.map(t => {
if (t.id === todo.id) {
todo.completed = completed;
}
return t;
});
}
}
export class AddTodo implements Action {
readonly type = ADD_TODO;
constructor(public payload: any) {}
}
export class RemoveTodo implements Action {
readonly type = REMOVE_TODO;
constructor(public payload: any) {}
}
addTodo(event: Todo) {
this.store.dispatch(new AddTodo(event));
}
export function reducer(
state = initialState, action: TodossAction
): TodosState {
switch (action.type) {
case ADD_TODO: {
const todo = action.payload;
const todos = [...state.todos, todo];
return { ...state, todos };
}
}
return state;
}
import { createSelector } from '@ngrx/store';
interface AppState { feature: FeatureState }
const getTodos = (state: FeatureState) => state.todos;
const getFeatureState = (state: AppState) => state.feature;
const getFeatureStateTodos = createSelector(
getFeatureState,
getTodos
);
export class TodosComponent implements OnInit {
todos$: Observable<Todo[]>;
constructor(private store: Store<TodosState>) {}
ngOnInit() {
this.todos$ = this.store.select(getTodos);
}
}
Trigger State Change
Request new data from backend
Response with new data
Save new data in store
Inform dialog component
export abstract class Store<State extends object> {
private readonly actionSource: Subject<Action<State>>;
private readonly stateSource: BehaviorSubject<State>;
readonly state$: Observable<State>;
constructor(initialState: State) {
this.stateSource = new BehaviorSubject<State>(
initialState
);
this.state$ = this.stateSource.asObservable();
this.actionSource = new Subject<Action<State>>();
this.actionSource.pipe(
observeOn(queueScheduler)
).subscribe(action => {
const currentState = this.stateSource.getValue();
const nextState = action(currentState);
this.stateSource.next(nextState);
});
}
select<SelectedState>(
selector: (state: State) => SelectedState
): Observable<SelectedState> {
return this.state$.pipe(
map(selector),
map(state => structuredClone(state)),
distinctUntilObjectChanged()
);
}
update(stateChanger: (state: State) => State) {
this.actionSource.next(stateChanger);
}
}
@Injectable({
providedIn: 'root',
})
export class TodoStoreService extends Store<TodoStoreState> {
constructor() {
super('TodoStore', { todos: [] });
}
addTodo(todo: Todo) {
this.update(state => ({
...state,
todos: [
...state.todos,
todo
],
}));
}
export class TodoComponent {
readonly allTodos$: Observable<Todo[]>;
constructor(
private todoService: TodoService,
private todoStore: TodoStoreService
) {
this.allTodos$ = this.todoStore.select(
state => state.todos
);
}
addTodo(description: string) {
this.todoService.addTodo(description);
}
}
export class TodoService {
constructor(
private todoAdapter: TodoAdapterService,
private todoStore: TodoStoreService
) { }
async addTodo(description: string) {
const todo = await lastValueFrom(
this.todoAdapter.addTodo(description)
);
this.todoStore.addTodo(todo);
}
}