Gian Marco Toso
Drinking coffee and saving the world. Software Engineer and professional geek
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
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
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...
One way data flow
A (better) FLUX variant
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
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
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 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
Each action is synchronously dispatched to the store
... but we know that things are mostly asynchronous!
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...
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
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
Here's an example
function getOrders() {
return async dispatch => {
const response = await http.get(
`${config.SERVER_ADDRESS}/orders`
)
dispatch(populateOrders(response.data))
}
}
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
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
Sagas can be used in Redux through the redux-saga middleware
Sagas are written using ES6's generators
An example straight from the docs
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);
}
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!
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 }
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'
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
}
}
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))
}
}
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!
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)
);
How does Redux fit in an Angular App?
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?
@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
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);
}
}
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(/* ... */) });
}
}
Remember to follow me :)
@gianmarcotoso
gianmarcotoso
By Gian Marco Toso
Slides for my talk at the 2017 AngularDay in Verona
Drinking coffee and saving the world. Software Engineer and professional geek