HSR CAS Frontend Engineering 2018
slides.com/dmodalek/hsr-cas-react-redux-august-2018
Dominic Modalek
Frontend Architect @ Namics AG
Current React & Redux Projects:
→ Example "1. React without Redux"
https://github.com/dmodalek/hsr-react-redux
Text
→ Use Redux for complex State that changes often
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'
}
}
}
The State in the Store is read-only
The only way to change the state 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 Provider component:
The connect() function wraps a presentational component and provides it with the store data it needs
<Provider store={storeInstance}>
<App />
</Provider>
export default connect(
mapStateToProps,
mapDispatchToProps
)(HomepageView)
Selectors are used on Container Components to get data from the Store and provide it to the Components.
For example getMovies() or getUpcomingMovies()
Arrays
.map()
.filter()
.reduce()
.find()
.findIndex()
.includes()
Objects
Object.keys()
(not ES6 but useful)
A higher-order component (HOC) is an advanced technique in React for reusing component logic.
A higher-order component is a function that takes a component and returns a new component.
Example: the Redux connect() function is a HOC that connects the wrapped Component to the Redux store.
const EnhancedComponent = higherOrderComponent(WrappedComponent);
// A HOC that logs props
const withLogProps = (WrappedComponent) => {
return class extends React.Component {
componentWillReceiveProps(nextProps) {
console.log('Current props: ', this.props);
console.log('Next props: ', nextProps);
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
// Use
const MyEnhancedComponent = withLogProps(MyComponent);
Recompose is a A React utility belt for function components and higher-order components — similar to what Lodash is for JavaScript.
// Optimized version of same component, using shallow comparison of props
// Same effect as extending React.PureComponent
const OptimizedComponent = pure(ExpensiveComponent)
// Even more optimized: only updates if specific prop keys have changed
const HyperOptimizedComponent = onlyUpdateForKeys(['propA', 'propB'])(ExpensiveComponent)
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().
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(doWork) // 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 Promises. Write async code that looks like synchronous code.
.
// Async Await
async componentDidMount() {
try {
await this.props.doLoadMovies();
await this.props.doLoadMovieDetails();
} catch(e) {
// whoops!
console.log("Could not load data...");
this.setState({ error: true });
}
}
// Promises
componentDidMount() {
this.props
.doLoadMovies()
.then(() => {
this.props.doLoadMovieDetails();
})
.catch(() => {
// whoops!
console.log("Could not load data...");
this.setState({ error: true });
});
}
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 immutable 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
React JS → Vue JS
https://github.com/vuejs/vue
Redux → MobX
https://github.com/mobxjs/mobx
Redux Thunk → Redux Saga
https://github.com/redux-saga/redux-saga
→ Migros MeinCoach
One React App with a Router
→ Pathé
Multiple React Apps