HSR CAS Frontend Engineering 2016
slides.com/dmodalek/hsr-cas-react-redux
Dominic Modalek
Senior Frontend Engineer @ Namics AG
React & Redux Projects:
Sunrise.ch und Pathé.ch
→ Example "1. React without Redux"
https://github.com/dmodalek/hsr-react-redux
Single source of truth
The state of your whole application is stored in a single object.
console.log(store.getState())
// Returns:
{
selectedMovies: [1, 3]
movies: {
1: {
title: 'A Hologram for the King'
},
2: {
title: 'How to Be Single'
},
3: {
title: 'The First Avenger - Civil War'
}
}
}
State is read-only
The only way to change the state in the store is to emit an action, an object describing what happened.
store.dispatch({
type: 'ADD_MOVIE',
title: 'X-Men: Apocalypse'
})
Actions describe the fact that something happened, but don't specify how the application's state changes in response. This is the job of a reducer.
function movieReducer(state = initialState, action) {
switch (action.type) {
case ADD_MOVIE:
return Object.assign({}, state, {
movies: [
...state.movies,
{
title: action.title
}
]
})
default:
return state
}
}
Action Creator is a function that returns an Action
// Without Action Creator
store.dispatch({
type: 'INCREMENT',
count: 1
})
// With Action Creator
const increment = (count) => ({
type: INCREMENT,
count
})
store.dispatch(increment(1))
Redux Thunk middleware
allows to dispatch Functions that have access to state and dispatch
const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
function increment() {
return {
type: INCREMENT_COUNTER
};
}
function incrementAsync() {
return (dispatch, getState) => {
setTimeout(() => {
// Yay! Can invoke sync or async actions with `dispatch`
dispatch(increment());
}, 1000);
};
}
// Thunk middleware provides dispatch and getState
return function (dispatch, getState) {
// Normal, sync action
dispatch(requestPosts(subreddit))
// The function called by the thunk middleware can return a value,
// that is passed on as the return value of the dispatch method.
// In this case, we return a promise to wait for.
return fetch(`http://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then((json) => {
// Update the app state with the results of the API call.
dispatch(receivePosts(subreddit, json))
})
}
Redux embraces the idea of separating presentational and container components. They are also known as Smart and Dumb components.
Container components connect to the store and provide data for the presentational components
Redux has no relation to React. You can write Redux apps with React, Angular, Ember, jQuery, or vanilla JavaScript.
React bindings are not included in Redux by default. You need to install them explicitly.
Provides the store functions to all components automatically via a higher order component <Provider store={store}>.
The connect() function wraps a presentational component and provides it with the store data it needs
Selectors are used on Container Components to get data from the Store and provide it to the presentational Component.
For example getMovies() or getUpcomingMovies()
Arrays
.map()
.filter()
.reduce()
.find()
.findIndex()
.includes()
Objects
Object.keys()
(not ES6 but useful)
var p = new Promise(function(resolve, reject) {
if (true) {
resolve('Success!');
} else {
reject('Failure!');
}
});
p.then(function(response) {
console.log('resolved:', response)
}).catch(function(error) {
console.log('rejected', error)
})
Resolve, Reject
Call resolve() within a Promise to mark it as fulfilled and optionally pass in a return value.
Call reject() within a Promise to mark it as rejected and optionally pass in a return error message.
Then, Catch
.then() is executed when the previous Promise was resolved
.catch() is executed when the previous Promise was rejected
Details
You can also use .then(success, error) to handle rejections by using the second argument for .then().
But using .catch() may be better readable. And using .then(success, error) doesn't handle errors emitted by the success handler of the same .then(success, error).
Another .then() or .catch() is needed to catch that error
doPromiseWork()
.then(doWork)
.then(doError)
.then(doWork) // this will be skipped
.then(doWork, errorHandler)
.then(deWork) // we resume here
.then(doError)
.then(doWork) // this will be skipped
.catch(errorHandler)
.then(doWork) // we resume here
Exceptions thrown inside of .then() and .catch() are converted to a rejection and reject the promise automatically. It's the same as calling reject() manually.
It therefore jumps to the next rejection handler (errorHandler) and skips over any potential success handlers.
After that, the code resumes with the next function .then(doWork) after the error handler
constructor (props) {
super(props)
props.doLoadTickets(props.site, props.sessionID)
.catch((error) => {
props.addNotification('error', error.message, 'tickets')
})
.then(() => {
return props.doLoadMember(props.site, props.sessionID)
})
.then(() => {
return Promise.all([
props.doLoadTickets(props.site, props.sessionID),
props.doGetItems(props.sessionID)
])
})
.then(() => {
return props.doAddLoyalityTicketsToSelection()
})
.catch(() => {
// do nothing
// - it is ok if there is no member
})
.then(() => {
return props.doGetOrder(props.sessionID)
})
.then(() => {
return props.doRehydrateOrder()
})
.catch(() => {
// do nothing
})
}
Async functions are a new feature scheduled to become a part of ES7.
They build on top of previous capabilities made available by ES6 (promises), letting you write async code as though it were synchronous.
// Async Await
async function doAsyncOp () {
var val = await asyncFunction();
val = await asyncFunction(val);
val = await asyncFunction(val);
return await asyncFunction(val);
};
// Promises
function doAsyncOp () {
return asyncFunction().then(function(val) {
return asyncFunction(val);
}).then(function(val) {
return asyncFunction(val);
}).then(function(val) {
return asyncFunction(val);
});
};
vs.
"If you go to this page, click on that button, then go back to the home page, grab a coffee, go to this page and click twice here, something weird happens"
Immutable data cannot be changed once created.
This leads to much simpler application development and change detection techniques.
+ Easier to Test
+ Performance
+ Predictability
+ Undo & Redo aka Time Travel
// create object one
let one = { completed: false }
// create a reference
var two = one;
// they now share the same reference
console.log(two === one) // true
// therefore changes done in one are reflected in the other
one.completed = true
console.log(two) // true
Objects in JavaScript are mutable and passed by reference.
Using an immutable approach would allow us to only compare the reference to check for Object equality.
Immutable JS is a helper library developed by Facebook that provides tools to create and manipulate immutable data structures.
It enforces the functional approach by forbidding objects modifications. With Immutable, when we want to update an object, we actually create another one with the modifications, and leave the original one as is.
// Immutable.js
// Example Immutable Object
var immutableObj = Immutable.Map({
one:'entry'
});
// Create a Reference
var immutableCopy = immutableObj;
// Still allows comparing references
console.log(immutableCopy === immutableObj); // true
// Set a value that is already the same is still the same reference
var immutableCopy = immutableObj.set('one', 'entry');
console.log(immutableCopy === immutableObj); // true
// Set a new value is a change and creates a new Object Reference
var immutableCopy = immutableObj.set('one', 'changed');
console.log(immutableCopy === immutableObj); // false
Automatically creates new Arrays or Objects when changing values.
...and without using a library like Immutable JS
// Copy
return [...list]
// Add Item
// - could also use .concat()
// - don't use .push() because it mutates the original Array
return [...list, item]
// Remove Item
// - don't use .splice() because it mutates the original Array
return [
...list.slice(0, index),
...list.slice(index + 1)
]
// Change Item
return [
...list.slice(0, index),
...item,
...list.slice(index + 1)
]
/**
* Copy
*/
// ES6 Object.assign()
Object.assign({}, myObject)
// ES7 Object Spread Operator
return {
...myObject
}
/**
* Add Prop
*/
// ES6 Object.assign()
Object.assign({}, myObject, { done: true })
// ES7 Object Spread Operator
return {
...myObject,
whatever: true
}
/**
* Remove Prop
*/
// ES6 Object.assign
Object.assign({}, myObject, { done: undefined })
// ES7 Object Spread Operator
return {
...myObject,
completed: undefined
}
/**
* Change Prop
*/
// ES6 Object.assign
Object.assign({}, myObject, { done: true })
// ES7 Object Spread Operator
return {
...myObject,
completed: true
}
// Nested data
[{
id: 1,
title: 'Some Article',
author: {
id: 1,
name: 'Dan'
}
}, {
id: 2,
title: 'Other Article',
author: {
id: 1,
name: 'Dan'
}
}]
Updating nested data is hard. Store it as normalized as possible.
// Normalized data
articleIDs: [1, 2],
articles: {
articles: {
1: {
id: 1,
title: 'Some Article',
author: 1
},
2: {
id: 2,
title: 'Other Article',
author: 1
}
},
users: {
1: {
id: 1,
name: 'Dan'
}
}
}
}
// Selector
export const getCategories = (state) => {
return state.tickets
.filter((ticket) => {
return isMainTicketCategory(ticket.id)
})
}
Use Selectors to build the data structure you need in your components
Redux on Egghead.io
https://egghead.io/courses/getting-started-with-redux
The excellent Redux FAQ
http://redux.js.org/docs/FAQ.html