State Management With Redux

A single source of truth for you Angular 4 app

Gian Marco Toso

@gianmarcotoso

gianmarcotoso

gianmarcotoso.com

polarityb.it

Software Engineer, The Pirate I Was Meant To Be

ūüé© About Me ūüé©

Born and raised in Turin, Italy

@gianmarcotoso

gianmarcotoso

gianmarcotoso.com

polarityb.it

MSc in Computer Engineering 

Self Employed Software Engineer

Researcher at ISMB

Javascript and PHP Developer

ūüćĒ About My Stack ūüćĒ

Docker

PHP/Laravel

"It was working on my machine!"

The monolith is here to stay

NodeJS/TypeScript

Seriously, it's awesome!

React + Redux

The Flux Capacitor!

I am not an Angular developer

sorry! :)

We cannot talk about Redux...

... without talking about FLUX

FLUX

One way data flow

REDUX

A (better) FLUX variant

One single Store

The standard FLUX implementation expects a store for each "domain" of the application

Each "domain" is managed by a specific Reducer

Redux has only one store, containing a single state which is itself divided in as many "domains" as required

Three Main Components

The State

The one single source of truth for your Redux application.

Actions

They are fed to the store to produce a new state by passing through one or more Reducers

Reducers

They are "fed" actions and produce a new state

Reducers are pure functions

A reducer is a pure function that takes in the current state and an action and returns a new state

import initialState from './DefaultState'

export default postsReducer = (state = initialState, action) => {
    switch (action.type) {
        case POPULATE_POSTS: {
            return { 
                ...state, 
                posts: action.posts
            }
        }

        default:
            return state
    }
}

Actions and Action Creators

Actions are plain objects, containing a type and, optionally, a payload

import {
    POPULATE_POSTS
} from './Actions'

export function populatePosts(posts) {
    return {
        type: POPULATE_POSTS,
        posts
    }
}

Actions are returned from Action Creators, which are functions that return the action itself

Actions are synchronous

Each action is synchronously dispatched to the store

... but we know that things are mostly asynchronous!

Enter: Middlewares

When an action is dispatched, it goes through every middleware registered on the store before reaching the reducers

Middlewares can be used for a lot of things, such as asynchronous API calls, logging, debugging...

The Thunk Middleware

One way to solve the asynchronous API call / synchronous action problem is to use the thunk middleware

The Thunk middleware intercepts actions that are functions instead of plain objects and calls them instead of forwarding them to the reducers

The Thunk Middleware

These functions, or thunks, receive the dispatch function as their argument, so they can dispatch a regular action (or another thunk!) when they are done

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

This is the actual code of the thunk middleware as written in the `redux-thunk` library

The Thunk Middleware

Here's an example

function getOrders() {
    return async dispatch => {
        const response = await http.get(
            `${config.SERVER_ADDRESS}/orders`
        )

        dispatch(populateOrders(response.data))
    }
}

Redux Saga

There are times when multiple actions need to be dispatched in a certain order to complete a specific user story

The sequence of the actions could be kept by calling an action creator from within another one (making them strongly coupled)

An external manager can also be used to orchestrate when an action needs to be called

Redux Saga

A saga is a sequence of actions representing a "user story"

A saga can be executed, paused or cancelled at any time

A saga is run by dispatching an Action, and can itself dispatch other actions 

Redux Saga

Sagas can be used in Redux through the redux-saga middleware

Sagas are written using ES6's generators

Redux Saga

An example straight from the docs

https://redux-saga.js.org/

function* fetchUser(action) {
   try {
      const user = yield call(Api.fetchUser, action.payload.userId);
      yield put({type: "USER_FETCH_SUCCEEDED", user: user});
   } catch (e) {
      yield put({type: "USER_FETCH_FAILED", message: e.message});
   }
}

function* mySaga() {
    yield takeEvery("USER_FETCH_REQUESTED", fetchUser);
}

Putting things together

To make a Redux application we need:

 

- At least one reducer

- Some actions that can be fed to the reducer

- A store for our state and to dispatch actions

Let's do this!

The Default State

It might make sense to reason about how the state of our domain should look like in the very beginning, so we'll define a default state

type IBlogState = {
    posts: Array<any>
}

const DefaultState : IBlogState = {
    posts: []
}   

export { IBlogState, DefaultState } 

Actions

After defining the default state, we can reason about which actions we want to be able to dispatch to make it evolve

export const POPULATE_POSTS = 'BLOG@POPULATE_POSTS'
export const ADD_POST = 'BLOG@ADD_POST'

Action Creators

Our actions need to be dispatched, so we define action creators. 

import { POPULATE_POSTS, ADD_POST } from './Actions'

export function populatePosts(posts) {
    return {
        type: POPULATE_POSTS,
        posts
    }
}

export function addPost(post) {
    return {
        type: ADD_POST,
        post
    }   
}

Action Creators

We also define our thunks as action creators, since they are dispatched the same way as regular actions

// ...
export function getPosts() {
    return async dispatch => {
        const res = await http.get(`${API}/api/posts`)

        dispatch(populatePosts(res.data))
    }
}
export function createPost(data) {
    return async dispatch => {
        const res = await http.post(`${API}/api/posts`, data)
        
        dispatch(addPost(res.data))
    }
}

The Reducer

Our reducer needs to be able to reduce a new state starting from the current state and an action

export function blogReducer(state = DefaultState, action) {
    switch(action.type) {
       case POPULATE_POSTS: {
            return {
                ...state,
                posts: action.posts
            }
       }   
       case ADD_POST: {
            return {
                ...state,
                posts: [
                    ...state.posts,
                    action.post
                ]
            }
       }
       default: return state
    }
}

I have not included any library (yet!)

 

All the code written so far is in plain JavaScript!

Creating The Store

We need to create a store to properly use our reducer(s)

import {
  applyMiddleware,
  Store,
  combineReducers,
  compose,
  createStore
} from 'redux'
import thunk from 'redux-thunk'
import { blogReducer, IBlogState } from './BlogSource'

interface IAppState {
    blog: IBlogState
}

export const store: Store<IAppState> = createStore(
  combineReducers({
      blog: blogReducer
  }),
  applyMiddleware(thunk)
);

Yay!

But what about Angular?

How does Redux fit in an Angular App?

Using Redux with Angular4

In order to use Redux with Angular2 we need to:

 

- Somehow provide an Angular2 component access to the store, so that it can access the state and dispatch actions

- Have some way to tell an Angular2 component when the state changes, so that it can update itself if required

How?

Enter Angular-Redux

@angular-redux/store is a library that solves exactly these problems, allowing us to

 

- provide the Redux store to any component as a dependency

 

- connect any component the state with a decorator

Angular-Redux

First, we add the NgReduxModule to our application module an initialize it with the store

import { NgReduxModule, NgRedux } from 'angular-redux/store'
import { store } from '@app/store'

@NgModule({
  /* ... */
  imports: [ /* ... */, NgReduxModule ]
})
class AppModule {
  constructor(ngRedux: NgRedux<IAppState>) {
    ngRedux.provideStore(store);
  }
}

Angular-Redux

We are now free to use Redux wherever we want. That's nice, isn'it it? :)

import { select } from '@angular-redux/store'
// ...

@Component({
  template: '<button (click)="onClick()">Add Post</button>'
})
class App {
  @select(['blog', 'posts']) posts$: Observable<Post>;

  constructor(private ngRedux: NgRedux<IAppState>) {}

  onClick() {
    this.ngRedux.dispatch({ type: ADD_POST, post: new Post(/* ... */) });
  }
}

Why Redux?

Thank you!

Questions?

Remember to follow me :)

@gianmarcotoso

gianmarcotoso

State Management With Redux

By Gian Marco Toso

State Management With Redux

Slides for my talk at the 2017 AngularDay in Verona

  • 1,138