Jorge Lucic (jvlucic)
fullstackDev @Witbooking
SIDE-EFFECTS
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
}
}
}
export default function thunkMiddleware({ dispatch, getState }) {
return next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
};
}
Text
Text
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;
}
}
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);
};
}
Redux thunk allows dispatching functions
Redux-promise allows dispatching promises
Redux-promise is more restrictive
export function* getMovies() {
yield* takeLatest(LOAD_MOVIES, fetchMovies);
}
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!
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});
...
}
...
}
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 );
}
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) {
});
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;
}
}
//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);
}
// 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);
}
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
{
"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"},
},
}
}
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",}
}
}
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))
)
);
"filters": {
"asyncFilters": { "movieQuery": "lego",}
},
"form": {
"syncFilters": {
"genre": { "value": "35"},
"year": { "value": "2015",}
}
}
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);
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
}
}
}
}
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
actions/
todos.js
components/
todos/
TodoItem.js
...
constants/
actionTypes.js
reducers/
todos.js
index.js
rootReducer.js
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 };
}
//ANGULAR
controllers/
directives/
services/
templates/
index.js
//RAILS
app/
controllers/
models/
views/