Skip to content

Instantly share code, notes, and snippets.

@killerchip
Last active January 21, 2018 21:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save killerchip/d5492084f20da23da12465d9569277d6 to your computer and use it in GitHub Desktop.
Save killerchip/d5492084f20da23da12465d9569277d6 to your computer and use it in GitHub Desktop.
Angular Template Code: A small "home made" redux store for simple/small angular applications.
export interface IAction {
type: number;
payload: any;
}

Home-made Redux Store

This is a simple/small Redux Store implementation for small angular apps. Use in case you don't want to use the full-blown Redux libraries out there, for the sake of simplicity.

This template code is based and inspired on Ultimate Angular - NGRX Store + Effects Online Training (Free). Highly recommended.

The example below is based on a super simple fictional 'Agenda' app where we can add contacts and to-do tasks.

Step 1: Copy the Home-Made Redux Store code

Copy into your application the following files, from this Gist that form the 'Home-Made Redux Store' class.

  • actions.interface.ts
  • reducer.interface.ts
  • reducers.interface.ts
  • home-made-redux-store.ts

Step 2: Define your state

Now define your AppState interface and its substates

export interface IContact {

    name: string;
    email: string;

}

export interface ITask {

    description: string;
    done: boolean;
    
}


export interface IAppState {
    
    contacts: IContact[];
    tasks: ITask[];

}

Step 3: Define the initial state

export const initialState: IAppState = {
    contacts: [
        {
            name: "Me",
            email: "me@example.com"
        }
    ],

    tasks: [
        {
            description: "Start populating this list",
            done: false
        }
    ]
}

Step 4: Define your action types

export enum ActionTypes {
    ADD_CONTACT,
    ADD_TASK
}

Step 5: Define your actions and their payloads

export class TaskAction implements IAction {
    public readonly type = ActionTypes.ADD_TASK

    constructor (
        public payload: ITask
    ){}
}

export class ContactAction implements IAction {

    constructor(
        public payload: IContact
    ) {
        
    }

    public readonly type = ActionTypes.ADD_CONTACT;

}

Step 6: Define your reducers (functions)

const contactsReducer: IReducer = function (state: IContact[], action: ContactAction) {
    switch (action.type) {
        case ActionTypes.ADD_CONTACT: {
            const contacts = [...state, action.payload];
            return contacts;
        }
    }
    return state;
};

const tasksReducer: IReducer = function (state: ITask[], action: TaskAction) {
    switch (action.type) {
        case ActionTypes.ADD_TASK: {
            const tasks = [...state, action.payload];
            return tasks;
        }
    }
    return state;
}

export const reducers: IReducers = {
    contacts: contactsReducer,
    tasks: tasksReducer
};

Step 7: Instanciate the store as your own service

In this example, the State Service extends the HomeMadeReduxStore class. Alternatively you could build a wrapper service that utilizes the HomeMadeReduxStore class.

@Injectable()
export class AppStateService extends HomeMadeReduxStore<IAppState> {

  constructor(
    reducers: IReducers = {},
    initialState: IAppState = <IAppState>{}
  ) { 
    super(reducers, initialState);
  }

}

Step 8: Inject your service into your module

In this example we instanciate the service with our specific state and reducers.

app.module.ts

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [
    {provide: AppStateService, useFactory(){
      return new AppStateService(reducers,initialState);
    }}
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Step 9: Start using the store by dispatching actions

import { TaskAction } from './app-state/actions/task-action';
import { AppStateService } from './app-state/app-state.service';
import { Component } from '@angular/core';
import { ContactAction } from './app-state/actions/contact-action';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'app';

  constructor(
    private appState: AppStateService
  ){

    console.log('Intial State:', this.appState.value);

    this.appState.dispatch(new ContactAction ({
      name: "John Doe",
      email: "johndoe@example.com"
    }));
    console.log('Adding contact', this.appState.value);

    this.appState.dispatch(new TaskAction ({
      description: 'Buy groceries',
      done: false
    }));
    console.log('Added task', this.appState.value);

  }

}
import { IReducers } from './reducers.interface';
import { IAction } from './action.interface';
export class HomeMadeReduxStore<S> {
/** Stores the state of the app. We consider that the state is constructed from 'slices'.
* 'Slices' are top level properties. E.g. a state with 2 'slices' could be:
* {
* contacts: Contact[];
* tasks: Task[];
* ui: any;
* }
*/
private state: S;
/** Reducers are specific functions, that process a specific 'slice' of the state. They:
* - Accept a specific 'slice' of the state
* - Modify the 'slice' based on the action
* - Return the modified 'sliced'
*
* The reducers object is hosting a list of reducers. Each reducer is defined in the property
* with the corresponding slice name.
* E.g.
* reducers['contacts'] = contactsReducer();
* reducers['tasks] = tasksReducer();
*/
private reducers: IReducers;
constructor(reducers: IReducers = {}, initialState: S = <S>{}) {
this.state = initialState;
this.reducers = reducers;
}
get value(): S {
return this.state;
}
dispatch (action: IAction) {
this.state = this.reduce(this.state, action);
}
/** Will iterate the reducers functions and reconstruct the new state
* with the 'slices' returned from each reducer.
*
* Note: When a reducer does operate on the state, then it returns it as is.
*/
private reduce (state: S, action: IAction): S {
const newState = {};
for (const slice in this.reducers) {
newState[slice] = this.reducers[slice](state[slice], action);
}
return <S>newState;
}
}
import { IAction } from './action.interface';
export interface IReducer {
(substate: any, action: IAction): any;
}
import { IReducer } from './reducer.interface';
export interface IReducers {
[key:string]: IReducer;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment