Redux Introduction II
Jorge Lucic (jvlucic)
fullstackDev @Witbooking
Give cheap excuse about why I won't say anything about ng-redux
What is this talk about?
-
Middlewares
-
Tools
-
What's next?
Redux Recap
Three Principles
- Single Source of truth
- State is read only
- Reducers are pure functions
Three Principles
- Single Source of truth
- State is read only
- Reducers are pure functions
Three Principles
- Single Source of truth
- State is read only
- Reducers are pure functions
SIDE-EFFECTS
Middlewares
Middleware is created by composing functionality that wraps separate cross-cutting concerns which are not part of your main execution task.
ASYNC
PERSISTENCE
LOGGING
HOW DO THEY WORK?
MIDDLEWARE
CHAIN
ACTION
REDUCER
applyMiddleware
is a store enhancer
applyMiddleware(thunk, promise, logger)
thunk( promise ( logger ( dispatch ) ) ) (action)
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, initialState, enhancer) => {
var store = createStore(reducer, initialState, enhancer)
var dispatch = store.dispatch
var chain = []
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
applyMiddleware(thunk, promise, logger)
thunk( promise ( logger ( dispatch ) ) ) (action)
export default function thunkMiddleware({ dispatch, getState }) {
return next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
};
}
RECAP
-
Middlewares handle cross-cutting concerns
-
Middlewares sit between an action dispatch and reducer
-
Middlewares in Redux are chained like in Express
But you don't need this . . .
Lets see some code...
Introducing
N&C
N&C gives you
LIFE-CHANGING ANSWERS
Nolan Comedies?
Hitchcock Romance?
Text
Text
SYNC FILTERS
MOVIE RESULTS
ASYNC FILTERS
Redux Thunk
import { fromJS } from 'immutable';
import { routerMiddleware } from 'react-router-redux';
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
export default function configureStore(initialState = {}, history) {
const createStoreWithMiddleware = compose(
applyMiddleware(routerMiddleware(history), thunk),
window.devToolsExtension ? window.devToolsExtension() : f => f
)(createStore);
const store = createStoreWithMiddleware(
createReducer(),
fromJS(initialState)
);
....
return store;
}
export function getMovies() {
return function (dispatch, getState) {
dispatch(loadMovies());
const txt = getState()
.get('filters')
.get('asyncFilters')
.get('movieQuery');
return mdb.searchMovie( {query: txt}, (err, res) => {
if(err){
dispatch(moviesLoadingError(err));
}
dispatch(moviesLoaded(normalize(res.results, schema.MOVIE_ARRAY)));
});
};
}
export function moviesLoaded(movies) {
return {
type: LOAD_MOVIES_SUCCESS,
movies: movies.result,
entities: movies.entities
};
}
function homeReducer(state = initialState, action) {
switch (action.type) {
case LOAD_MOVIES:
return state
.set('loading', 'true')
.set('error', false)
.set('movies', false);
case LOAD_MOVIES_SUCCESS:
return state
.set('movies', action.movies)
.set('loading', false);
case LOAD_MOVIES_ERROR:
return state
.set('error', action.error)
.set('loading', false);
default:
return state;
}
}
Redux Promise
import { fromJS } from 'immutable';
import { routerMiddleware } from 'react-router-redux';
import { createStore, applyMiddleware, compose } from 'redux';
import promiseMiddleware from 'redux-promise';
export default function configureStore(initialState = {}, history) {
const createStoreWithMiddleware = compose(
applyMiddleware(routerMiddleware(history), promiseMiddleware),
window.devToolsExtension ? window.devToolsExtension() : f => f
)(createStore);
const store = createStoreWithMiddleware(
createReducer(),
fromJS(initialState)
);
....
return store;
}
export function getMovies(txt) {
return {
type: LOAD_MOVIES_SUCCESS,
payload: searchMovie(txt).then(
(res) => {
const movies = normalize(res.results, schema.MOVIE_ARRAY);
return {
movies: movies.result,
entities: movies.entities
}
}
)
}
}
function homeReducer(state = initialState, action) {
switch (action.type) {
case LOAD_MOVIES:
return state
.set('loading', 'true')
.set('error', false)
.set('movies', false);
case LOAD_MOVIES_SUCCESS:
return state
.set('movies', action.payload.movies)
.set('loading', false);
case LOAD_MOVIES_ERROR:
return state
.set('error', action.error)
.set('loading', false);
default:
return state;
}
}
export default function thunkMiddleware({ dispatch, getState }) {
return next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
};
}
export default function promiseMiddleware({ dispatch }) {
return next => action => {
if (!isFSA(action)) {
return isPromise(action)
? action.then(dispatch)
: next(action);
}
return isPromise(action.payload)
? action.payload.then(
result => dispatch({ ...action, payload: result }),
error => dispatch({ ...action, payload: error, error: true })
)
: next(action);
};
}
RECAP
-
Redux thunk allows dispatching functions
-
Redux-promise allows dispatching promises
-
Redux-promise is more restrictive
Redux Saga
What is?
-
Sagas are like background processes
export function* getMovies() {
yield* takeLatest(LOAD_MOVIES, fetchMovies);
}
What is?
-
Sagas yield effects
yield call(mdb.searchMovie, query);
// Effect -> call the function mdb.searchMovie with query obj as argument
{
CALL: {
fn: mdb.searchMovie,
args: [query]
}
}
//Effects are like actions to the saga middleware!
Sagas use Generators
export function* fetchMovies(getState) {
...
queryObj = { ...queryObj, ...( yield buildPeopleQuery(peopleQuery) ) };
...
}
function* buildPeopleQuery(people){
...
for (let i = 0; i < people.length; i++) {
...
let castId = yield cps([mdb, mdb.searchPerson], {query: person.query});
...
}
...
}
Generators Detour
function *main(){
var search_terms = yield Promise.all( [
request( "http://some.url.1" ),
request( "http://some.url.2" ),
request( "http://some.url.3" )
] );
var search_results = yield request(
"http://some.url.4?search=" + search_terms.join( "+" )
);
var resp = JSON.parse( search_results );
}
Turn ASYNC into SYNC
It's not so simple...
It's not so simple...
Q.spawn(function* () {
let result = yield asyncFunc('http://example.com');
console.log(result);
});
co(function* getResults(){
var a = read('index.js', 'utf8');
var b = request('http://google.ca');
var res = yield [a, b];
}).catch(function (ex) {
});
What is?
-
Reducers are responsible for handling state transitions between actions.
-
Sagas are responsible for orchestrating complex/asynchronous operations.
import { fromJS } from 'immutable';
import { routerMiddleware } from 'react-router-redux';
import { createStore, applyMiddleware, compose } from 'redux';
import promiseMiddleware from 'redux-promise';
import sagaMiddleware from 'redux-saga';
import sagas from './sagas';
export default function configureStore(initialState = {}, history) {
const createStoreWithMiddleware = compose(
applyMiddleware(routerMiddleware(history), sagaMiddleware(...sagas)),
window.devToolsExtension ? window.devToolsExtension() : f => f
)(createStore);
const store = createStoreWithMiddleware(
createReducer(),
fromJS(initialState)
);
....
return store;
}
export function loadMovies() {
return {
type: LOAD_MOVIES
};
}
export function moviesLoaded(movies) {
return {
type: LOAD_MOVIES_SUCCESS,
movies: movies.result,
entities: movies.entities
};
}
export function* getMovies() {
yield* takeLatest(LOAD_MOVIES, fetchMovies);
}
export function* fetchMovies() {
const filters = yield select(filtersSelector);
let txt = filters.get('asyncFilters').get('movieQuery');
try{
let res = yield cps([mdb, mdb.searchMovie ], {query: txt});
yield put(moviesLoaded(normalize(res.results, schema.MOVIE_ARRAY)));
}catch (e){
yield put(moviesLoadingError(e));
}
}
function homeReducer(state = initialState, action) {
switch (action.type) {
case LOAD_MOVIES:
return state
.set('loading', 'true')
.set('error', false)
.set('movies', false);
case LOAD_MOVIES_SUCCESS:
return state
.set('movies', action.movies)
.set('loading', false);
case LOAD_MOVIES_ERROR:
return state
.set('error', action.error)
.set('loading', false);
default:
return state;
}
}
What is?
-
Reducers are responsible for handling state transitions between actions.
-
Sagas are responsible for orchestrating complex/asynchronous operations.
Execution Flows
//PARALLEL
const [users, repos] = yield [
call(fetch, '/users'),
call(fetch, '/repos')
]
//RACE
function* fetchPostsWithTimeout() {
const {posts, timeout} = yield race({
posts : call(fetchApi, '/posts'),
timeout : call(delay, 1000)
})
}
//CANCELLATION
export function* getMovies() {
yield* takeLatest(LOAD_MOVIES, fetchMovies);
}
Why use Middlewares at all?
// action creator
// needs to dispatch, so it is first argument
function loadData(dispatch, userId) {
return fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
);
}
// component
componentWillMount() {
// don't forget to pass dispatch
loadData(this.props.dispatch, this.props.userId);
}
RECAP
-
Sagas allow implementing complex flows
-
Sagas embrace generators, hiding callback abstractions
-
Sagas are easier to test
-
If your side-effects are simple, stick to thunks
Redux Tools
Normalizr
Flatten your nested entities
{
"name": "Season 1",
"overview": "The first season of the American television drama series Breaking Bad premiered...",
"id": 3572,
"season_number": 1,
"episodes": [
{
"name": "Pilot",
"overview": "When an unassuming high school chemistry...",
"id": 62085,
"season_number": 1,
"vote_average": 8.5,
"crew": [
{
"id": 2483,
"name": "John Toll",
"department": "Camera",
"job": "Director of Photography",
"profile_path": null
}
...
],
"episode_number": 1,
"guest_stars": [
{
"id": 92495,
"name": "John Koyama",
"character": "Emilio Koyama",
"order": 1,
"profile_path": "/uh4g85qbQGZZ0HH6IQI9fM9VUGS.jpg"
}
...
]
},
{"name": "Other Episode 1"},
{"name": "Other Episode 2"},
{"name": "Other Episode 3"},
{"name": "Other Episode 4"},
{"name": "Other Episode 5"}
]
}
const {Schema, arrayOf, } = require('normalizr');
const season = new Schema('seasons');
const episode = new Schema('episodes');
const crewMember = new Schema('crew');
const guestStar = new Schema('guestStars');
season.define({
episodes: arrayOf(episode)
});
episode.define({
crew: arrayOf(crewMember),
guest_stars: arrayOf(guestStar)
});
exports.season = season;
{
"result": 3572,
"entities": {
"seasons": {
"3572": {
"air_date": "2008-01-19",
"episodes": [62085,62086,62087,62088,62089,62090],
"name": "Season 1",
"overview": "The first season of the American television drama series Breaking Bad premiered ...",
"id": 3572,
"season_number": 1
}
},
"crew": {
"2483": {
"id": 2483,
"credit_id": "52b7029219c29533d00dd2c1",
"name": "John Toll",
"department": "Camera",
"job": "Director of Photography",
"profile_path": null
},
...
}
"guest_stars":{
"92495": {
"id": 92495,
"name": "John Koyama",
"credit_id": "52542273760ee3132800068e",
"character": "Emilio Koyama",
"order": 1,
"profile_path": "/uh4g85qbQGZZ0HH6IQI9fM9VUGS.jpg"
},
...
}
"episodes": {
"62085": {
"episode_number": 1,
"name": "Pilot",
"overview": "When an unassuming high school chemistry ...",
"id": 62085,
"season_number": 1,
"vote_average": 8.5,
"crew": [ 2483, 66633, 66633, 1280071 ],
"guest_stars": [92495,1223192,1216132,161591,1046460,1223197,61535,115688]
},
"62086": {"name": "Other Episode 1"},
"62087": {"name": "Other Episode 2"},
"62088": {"name": "Other Episode 3"},
"62089": {"name": "Other Episode 4"},
"62090": {"name": "Other Episode 5"},
},
}
}
Why?
-
Nested entities are bad for redux.
-
Hard to keep track of mutations
-
Hard to keep components isolated
-
Hard to update shared entities
-
Hard to cache entities
-
-
Smaller JSON, easier to reason about
Reselect
MapStateToProps
Input: State
Output: Props for component
const mapStateToProps = (state) => {
const {
home:{ movies: allMovieIds, selectedMovies, loading, error } = {},
form: { syncFilters: {genre, score, year} = { } } = {},
entities: {
movies: movieEntities
} = {}
} = state.toJS() ; // ANTI-PATTERN!
const allMovies = allMovieIds && allMovieIds.map( id => movieEntities[id]);
const movies = allMovies && allMovies
.filter(byGenre(genre.value || null))
.filter(byScore(score.value || null))
.filter(byYear(year.value || null));
return { movies, selectedMovies, loading, error}
};
"filters": {
"asyncFilters": { "movieQuery": "lego",}
},
"form": {
"syncFilters": {
"genre": { "value": "35"},
"year": { "value": "2015",}
}
}
MapStateToProps
Executes on each KeyPress
SOLUTION:
Create Memoized Selectors
const homeSelector = (state) => state.get('home');
export const moviesResultsSelector = createSelector(
homeSelector,
movieEntitiesSelector,
(home, movieEntities) => home.get('movies')
&& home.get('movies').map( id => movieEntities[id] )
);
export const filteredMoviesResultsSelector = createSelector(
moviesResultsSelector, genreFilterSelector,scoreFilterSelector,
searchTextFilterSelector,yearFilterSelector,
(movies, genre, score, search, year) => (
movies && movies
.filter(byGenre(genre))
.filter(byScore(score))
.filter(byYear(year))
)
);
"filters": {
"asyncFilters": { "movieQuery": "lego",}
},
"form": {
"syncFilters": {
"genre": { "value": "35"},
"year": { "value": "2015",}
}
}
...
export const genreFilterSelector = createSelector(
syncFiltersSelector,
(syncFilters) => ( syncFilters && parseInt(syncFilters.genre.value) || null )
);
...
export const filteredMoviesResultsSelector = createSelector(
moviesResultsSelector, genreFilterSelector,scoreFilterSelector,searchTextFilterSelector,yearFilterSelector,
(movies, genre, score, search, year) => (
movies && movies
.filter(byGenre(genre))
.filter(byScore(score))
.filter(byYear(year))
)
);
Composition
"filters": {
"asyncFilters": { "movieQuery": "lego",}
},
"form": {
"syncFilters": {
"genre": { "value": "35"},
"year": { "value": "2015",}
}
}
Why?
-
Increase Performance
-
Keep your state minimal
-
Reusability
ImmutableJS
var map1 = Immutable.Map({a:1, b:2, c:3});
var map2 = map1.set('b', 2);
assert(map1.equals(map2) === true);
var map3 = map1.set('b', 50);
assert(map1.equals(map3) === false);
We are only interested in doing work when something has changed
Why?
-
Performance Optimizations!
-
Mutations make everything harder to debug
-
Mutations do not trigger selectors
-
Mutations Kill time travel
-
If you are using React it works great with should ComponentUpdate / recompose pure
Redux Forms
function AsyncFilters(props) {
return (
...
<TextField hintText="Movie name, plot" onChange={props.onChangeMovieQuery}/>
...
)
}
<AsyncFilters onSearch={onSearch} onChangeMovieQuery={onChangeMovieQuery} ... />
function mapDispatchToProps(dispatch) {
return {
...
onChangeMovieQuery: (evt) => {
dispatch(changeMovieQueryFilter(evt.target.value))
},
...
};
}
export function changeMovieQueryFilter(text) {
return {
type: CHANGE_MOVIE_QUERY_FILTER,
text
};
}
function filtersReducer(state = initialState, action) {
switch (action.type) {
case CHANGE_MOVIE_QUERY_FILTER:
return state
.setIn(['asyncFilters', 'movieQuery'], action.text);
...
}
}
class SyncFilters extends Component {
render() {
const {fields: {searchText, genre, year, score}, handleSubmit, resetForm} = this.props;
return (
<SearchBar label="Name" placeholder={"Filter"} {...searchText}/>
<select type="select" label="Genre" placeholder="Pick a Genre"
value={genre.value || ''} {...genre}>
<option>Pick a Genre</option>
{ ... }
</select>
<select type="select" label="Year" placeholder="Pick a Year"
value={year.value || ''} {...year}>
<option >Pick a Year</option>
{ ... }
</select>
);
}
}
SyncFilters = reduxForm({
form: 'syncFilters',
fields: ['searchText', 'genre', 'year', 'score']
})(SyncFilters);
{
"form": {
"syncFilters": {
"genre": {
"visited": true,
"value": "16",
"touched": true
},
"year": {
"visited": true,
"value": "2013",
"touched": true
}
}
}
}
Recap
-
Tools are a big help for perfomance and DX
-
Normalizr simplifies your nested data models helping with one source of thruth and easy updates
-
Selectors are great for reusability and optimization
-
ImmutableJS helps to keep mutations at bay
-
Redux-form obliterate lots of boilerplate code
What's next
START YOUR OWN PROJECT!
Code Structure
actions/
todos.js
components/
todos/
TodoItem.js
...
constants/
actionTypes.js
reducers/
todos.js
index.js
rootReducer.js
ENTER DUCKS!
const LOAD = 'my-app/widgets/LOAD';
const CREATE = 'my-app/widgets/CREATE';
const UPDATE = 'my-app/widgets/UPDATE';
const REMOVE = 'my-app/widgets/REMOVE';
export default function reducer(state = {}, action = {}) {
switch (action.type) {
// do reducer stuff
default: return state;
}
}
export function loadWidgets() {
return { type: LOAD };
}
export function createWidget(widget) {
return { type: CREATE, widget };
}
export function updateWidget(widget) {
return { type: UPDATE, widget };
}
export function removeWidget(widget) {
return { type: REMOVE, widget };
}
Going a bit further
Looks Familiar?
//ANGULAR
controllers/
directives/
services/
templates/
index.js
//RAILS
app/
controllers/
models/
views/
Please Read This!
Picking a boilerplate...
My two cents..
START YOUR OWN!!
Know your tools!
- Tech and frameworks change everyday, underlying principles not so often
- You may be getting much more that you bargained for.
- Wow, webpack loading times +40s
- Boilerplates are opinionated, have your own opinion!
- You will have to change something at some point.
Thats it for today...
Thank you for coming :)
Advanced-Redux
By Jorge Lucic
Advanced-Redux
- 961