React
& Redux
HSR CAS Frontend Engineering 2018
slides.com/dmodalek/hsr-cas-react-redux-august-2018
Content
- Why Redux?
- Redux Basics
- Redux Advanced
- Async Flow
- Data Handling
About Me
Dominic Modalek
Frontend Architect @ Namics AG
Current React & Redux Projects:
Why Redux?
React without Redux
-
Concepts State, Props and Lifecycle Methods
-
Component Communication via parent component and Props
-
App State is simple and matches the Component structure
→ Example "1. React without Redux"
https://github.com/dmodalek/hsr-react-redux
Component Communication without Redux
App State Management
without Redux
- A lot of the time your app's state tree could be considerably different than the UI tree
-
Leaf components need to access state that their parents don't need
-
Many components need access to the same data
- React's state management is enough for UI state that only this one component needs
React vs. Redux
What the author of Redux says
React Context 🆕
- React 16.3 ships with a new Context API
- Solves this Problem of Prop Drilling
- Use it as a simpler Alternative to Redux / MobX
- State should be simple & static i.e. Themes, App Config
Why Redux?
Text
- Redux provides a lot more than just state for leaf components
- Redux encourages the use of concepts like Container, Actions, Reducers, Selectors and Immutability that help manage complex state
- Many of those concepts are also used in other libraries for example MobX
→ Use Redux for complex State that changes often
Redux Basics
Redux Flow
Redux Flow
- Store
- Actions
- Reducer
Redux Concepts
Single source of truth
The state of your whole application is stored in a single object.
1. Store
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.
2. Actions
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.
3. 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
}
}
Praxisübung
Redux Advanced
Async
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))
Async
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))
})
}
Async
Praxisübung
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
Presentational &
Container Components
Presentational &
Container Components
React-Redux Bindings
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.
React-Redux Bindings
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
Selectors are used on Container Components to get data from the Store and provide it to the Components.
For example getMovies() or getUpcomingMovies()
ES 6 Functions
Arrays
.map()
.filter()
.reduce()
.find()
.findIndex()
.includes()
Objects
Object.keys()
(not ES6 but useful)
Praxisübung
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.
Higher Order Components
const EnhancedComponent = higherOrderComponent(WrappedComponent);
Higher Order Components
// 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);
Higher Order Components
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)
Async Flow
Promises
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)
})
Promises
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
Promises
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
Promise
Chain
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
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 });
});
}
Praxisübung
Data Handling
Mutable
vs.
Immutable Data
"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
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
Mutable Data
// 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
Immutable JS
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
// 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.
How to change Arrays and Objects without mutating them?
...and without using a library like Immutable JS
Immutable Arrays methods
// 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)
]
Immutable Object methods
/**
* 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
}
Normalize Data
// 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'
}
}
}
}
Normalized Data & Selectors
// 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
Praxisübung
Recap
-
Only use Redux when you need it
- Redux architecture revolves around a strict unidirectional data flow.
- Redux provides store.getState(), store.dispatch() and store.subscribe()
- Use Container Components to provide data to Presentational Components
-
Reducers in Redux are Pure Functions
- Use immutable methods to change Redux state
- Store data normalized and use Selectors to get data for the components
Ressources
Redux on Egghead.io
https://egghead.io/courses/getting-started-with-redux
The excellent Redux FAQ
http://redux.js.org/docs/FAQ.html
Alternatives
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
Project Architecture
→ Migros MeinCoach
One React App with a Router
→ Pathé
Multiple React Apps
Merci 🎉
HSR CAS — React & Redux August 2018
By Dominic Modalek
HSR CAS — React & Redux August 2018
Slides for the React Redux course at HSR
- 562