Creating a Redux Like Store

Published on

In this post we will take the state store created in the previous post make more redux like. The first thing we need to answer is what is redux?.

According the to offical redux website:

Redux is a predictable state container for JavaScript apps. It helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test.&

In simple terms, redux is a pattern to follow to manage global application data which can be shared across multiple components.

The Pieces That Make the Redux Pattern

There are four parts to the Redux pattern each of which serve a specific function….

  1. Actions: You can think of Actions as requests/events to perform operations against the state data. Actions generally have type property describe the action to be performed and an optional payload of data that is needed to perform that Action.

  2. Reducers: Reducers are the Action handlers (or event handlers), that actually perform the operations based on the Action type. Generally this a a function that takes in the current state and the action and returns the new state.

  3. Selectors: Selectors are functions that map a specific piece of state from the overall state. We will be using Rxjs with this example so our selectors will return Observables for that piece of state allowing us to receive updates to that state data when they occur.

  4. Store: The Store is the a global object container that holds all the state data and provides an API to dispatch actions and select state.

 

Below is a diagram displaying the redux flow

Redux flow

 

Redux Implementation

We need to create types for an Action, Reducer, and Selector. Our Action type will have two proeprties, a type which describes the action to be preformed, and payload which is any additional data that is needed to perform that action/operation.

Our Reducer type is a function that takes in the current state value T and an Action type and returns a new state value T.

And our Selector type is another function type that takes in the current state value and returns some value based on that state.

// store.ts

export interface Action {
  type: string;
  payload?: any;
}

export interface Reducer<T> {
  (state: T, action?: Action): T;
}

export interface Selector<T, R> {
  (state: T): R;
}

Now that we have our basic types defined we can create our AbstractStore. We’re going to use the AbstractStore from the previous post with some slight modifications.

We will add a dispatch method that takes in an Action and runs our current state and that action through a reducer to get our new state.

We also modified our select method to use our Selector type instead of using a mapping function.

Our new AbstractStore implementation will something like…

// store.ts

export abstract class AbstractStore<T> {
  protected state: T;
  protected stateSource: BehaviorSubject<T>;
  protected reducer: Reducer<T>;

  constructor() {
    this.state = this._deepFreeze(this.state);
    this.stateSource = new BehaviorSubject<T>(this.state);
  }

  public setState(state: T): void {
    this.state = this._deepFreeze(state);
    this.stateSource.next(this.state);
  }

  public dispatch(action: Action): void {
    const state: T = this.reducer(this.state, action);
    this.setState(this.reducer(this.state, action));
  }

  public select<R>(selector: Selector<T, R>): Observable<R> {
    return this.stateSource
      .asObservable()
      .pipe(map(selector), distinctUntilChanged());
  }

  private _deepFreeze<T>(obj: T): T {
    const propNames = Object.getOwnPropertyNames(obj);
    for (let name of propNames) {
      let value = (obj as any)[name];
      if (value && typeof value === 'object') {
        this._deepFreeze(value);
      }
    }
    return Object.freeze(obj);
  }
}

Now we could accept the inital state and a reducer as constructor arguments (inversion of control); this would allow our store to be more testable. We could also create a decorator which will accept these values as well. Either approach will work.

Below is code for the decorator approach…

// store.ts

export interface StoreConfig {
  initialState: any;
  reducer: Reducer<any>;
}

export function Store(config: StoreConfig) {
  return function (constructor: Function) {
    constructor.prototype.state = config.initialState;
    constructor.prototype.reducer = config.reducer;
  };
}

Creating a Todos Store

Now we can create our Todos store. We’ll create actions to handle our basic Todo CRUD operations; creating, update, and removing todos, and selectors for selecting Todos.

// todos.actions.ts

import { Action } from './store';
import { Todo } from './todos.models';

export enum TodosActionTypes {
  ADD_TODO = '[TODOS] Add Todo',
  REMOVE_TODO = '[TODOS] Remove Todo',
  UPDATE_TODO = '[TODOS] Update Todo',
}

export class AddTodo implements Action {
  public type: TodosActionTypes = TodosActionTypes.ADD_TODO;
  constructor(public payload: Todo) {}
}

export class RemoveTodo implements Action {
  public type: TodosActionTypes = TodosActionTypes.REMOVE_TODO;
  constructor(public payload: Todo) {}
}

export class UpdateTodo implements Action {
  public type: TodosActionTypes = TodosActionTypes.UPDATE_TODO;
  constructor(public payload: Todo) {}
}

And our Todo reducer to handle the above actions…

// todos.reducer.ts

import { Reducer, Action } from './store';

import { TodosActionTypes } from './todos.actions';
import { Todo } from './todos.models';

export interface TodosState {
  todos: Todo[];
}

export const initialTodosState: TodosState = {
  todos: [],
};

export const todosReducer: Reducer<TodosState> = (
  state: TodosState = initialTodosState,
  action?: Action
) => {
  switch (action?.type) {
    case TodosActionTypes.ADD_TODO:
      return {
        ...state,
        todos: [...state.todos, action.payload],
      };
    case TodosActionTypes.REMOVE_TODO:
      return {
        ...state,
        todos: state?.todos?.filter((todo) => todo?.id !== action?.payload?.id),
      };
    case TodosActionTypes.UPDATE_TODO:
      return {
        ...state,
        todos: state?.todos?.map((todo) =>
          todo.id === action?.payload?.id ? action.payload : todo
        ),
      };
    default:
      return state;
  }
};

Now that we have actions and a reducer, we can create our TodosStore which will extend our AbstractStore and will be decorated with our @Store decorator.

// todos.store.ts

import { initialTodosState, todosReducer, TodosState } from './todos.reducer';
import { AbstractStore, Store } from './store';
import { Injectable } from '@angular/core';

@Store({
  initialState: initialTodosState,
  reducer: todosReducer,
})
@Injectable({ providedIn: 'root' })
export class TodosStore extends AbstractStore<TodosState> {}

Our TodosStore will maintain our Todo’s state, and allows us to dispatch actions to manipulate that state, and select values from that state. Now all we need are some selectors to access and receive update to state changes.

We’ll create three selectors

  1. A selector to select all todos
  2. A selector to select completed todos
  3. A selector to select incomplete todos.
import { Selector } from './store';
import { Todo } from './todos.models';
import { TodosState } from './todos.reducer';

export const selectTodos: Selector<TodosState, Todo[]> = (
  state: TodosState
): Todo[] => state.todos;

export const selectIncompleteTodos: Selector<TodosState, Todo[]> = (
  state: TodosState
): Todo[] => state.todos.filter((todo) => !todo.isComplete);

export const selectCompleteTodos: Selector<TodosState, Todo[]> = (
  state: TodosState
): Todo[] => state.todos.filter((todo) => todo.isComplete);

Now we can use our TodosStore in our Angular app…

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import {
  TodosStore,
  AddTodo,
  UpdateTodo,
  RemoveTodo,
  Todo,
  selectCompleteTodos,
  selectIncompleteTodos,
} from '../../../store';
import { buildTodoForm } from '../../components/todo-form/todo-form.builder';

@Component({
  selector: 'app-todos',
  templateUrl: './todos.component.html',
  styleUrls: ['./todos.component.css'],
})
export class TodosComponent implements OnInit {
  public todos$: Observable<Todo[]>;
  public completed$: Observable<Todo[]>;
  public todosForm: FormGroup;

  constructor(
    private _todosStore: TodosStore,
    private _formBuilder: FormBuilder
  ) {}

  ngOnInit(): void {
    this.todos$ = this._todosStore.select(selectIncompleteTodos);
    this.completed$ = this._todosStore.select(selectCompleteTodos);
    this.todosForm = buildTodoForm(this._formBuilder);
  }

  public createTodo(todo: Todo): void {
    todo.id = this._generateTodoId(10000);
    todo.isComplete = false;
    this._todosStore.dispatch(new AddTodo(todo));
    this.todosForm.reset();
  }

  public updateTodo(todo: Todo): void {
    this._todosStore.dispatch(new UpdateTodo(todo));
  }

  public deleteTodo(todo: Todo): void {
    this._todosStore.dispatch(new RemoveTodo(todo));
  }

  private _generateTodoId(max) {
    return Math.floor(Math.random() * max);
  }
}

Below is an embedded Stackblitz with a working example of out TodosStore being used…

Our todo example in a working Stackblitz

comments powered by Disqus