Redux
The story about state
MV* (Angular, Backbone, Ember)
- Model: manages the behavior and data of the application domain
- View: represents the display of the model in the UI
- Controller: takes user input, manipulates the model and causes the view to update
What's wrong?
UI
Filters
Search Results
MV* (Angular, Backbone, Ember)
Sorting
UI
UI
Tracking
Log
Validation
Locale
Tabs
Super crazy requirement
What's wrong?
MV* (Angular, Backbone, Ember)
- application state in multiple places
- application state is not predictable
- easy to break something by accident because of dependencies
- hard to track bugs
- hard to explain application logic to newcomers
Something happened
State changed
UI is rendered
UI is rendered
Something happened
State changed
UI is rendered
UNI-DIRECTIONAL
DATA FLOW
New mindset
Here comes Redux
Application state is:
ONE
SINGLE
JAVASCRIPT
OBJECT
READ-ONLY
Common Redux misconception: state is held in a “giant object”. It’s just object referencing a few other objects. Nothing giant about it.
Dan Abramov (creator of Redux)
How to change the state
CURRENT
STATE
PURE
FUNCTION*
ACTION
NEW STATE
* No side effects
Reducer
const newState = reducer(currentState, action);
// action
{ type: 'ACTION TYPE', /*...*/ }
YOU SHALL NOT MUTATE THE STATE
TODO app example
// state of simple TODO app
const initialState = {
visibilityFilter: 'all',
todos: []
};
function todosReducer(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
const todoItem = {
text: action.text,
completed: false
};
return R.append(todoItem, state.todos);
default:
return state;
}
}
R.append('tests', ['write', 'more']);
//=> ['write', 'more', 'tests']
R.assoc('c', 3, {a: 1, b: 2});
//=> {a: 1, b: 2, c: 3}
Redux store
import { createStore } from 'redux';
const store = createStore(reducer, initialState);
Store
getState()
subscribe()
dispatch()
subscribe to state changes in UI
get the current state of the app
dispatch an action to reducers
Selectors
{
products: [
{
id: 1,
title: 'Product 1',
description: 'Lorem ipsum',
price: 1000
},
{
id: 2,
title: 'Product 2',
description: 'Lorem ipsum',
price: 2000
}
],
user: {
name: 'John Doe',
role: 'admin',
currency: 'EUR'
},
basket: {
items: [1, 1, 2],
totalPrice: 4000
}
}
- Selectors extracts parts of the store
- Efficient
- Composable
import { createSelector } from 'reselect'
const productsSelector = state => state.products;
const basketItemsSelector = state => state.basket.items;
const totalProductsSelector = createSelector(
productsSelector,
(items) => R.length(items)
)
const basketViewSelector = createSelector(
productsSelector,
basketItemsSelector,
(products, basketItems) => {
// produce data needed by the view
}
)
Redux data flow
Action
Reducer
Store
View
<input ng-model="vm.text">
<a ng-click="vm.addTodo(vm.text)">
add
</a>
<todo-item todo="todo"
ng-repeat="todo in vm.todos">
</todo-item>
{
type: 'ADD TODO',
text: 'Learn Redux'
}
(state, action) => newState
{
visibilityFilter: 'all',
todos: ['Learn Redux']
}
Learn Redux
add
add
Learn Redux
Why Redux?
- leverages functional programming principles
- single source of truth (store)
- easy to reason about the application state
- easy to test
- you can use it with vanilla JS, Angular, React or whatever
- very small footprint and API surface (228 Bytes min+gzip)
Time Traveling
+
Hot Module Reloading
Connecting UI to Redux
import ...
angular.module('app', ['ngRedux', 'app.todos'])
.config(($ngReduxProvider) => {
const rootReducer = combineReducers({todos, user});
$ngReduxProvider.createStoreWith(rootReducer);
});
angular.module('app.todos', ['ngRedux'])
.controller('TodosCtrl', ($ngRedux, $scope) => {
let unsubscribe = $ngRedux.connect(sliceOfTheState, availableActions)(this);
$scope.$on('$destroy', unsubscribe);
});
Redux bindings:
How to start with Redux
- Design your store
- List your actions
- Create reducers
- Connect UI to store
{
products: [
{
id: 1,
title: 'Product 1',
description: 'Lorem ipsum',
price: 1000
},
{
id: 2,
title: 'Product 2',
description: 'Lorem ipsum',
price: 2000
}
],
user: {
name: 'John Doe',
role: 'admin',
currency: 'EUR'
},
basket: {
items: [1, 1, 2],
totalPrice: 4000
}
}
function addItemToBasket(itemId) {
return {
type: 'ADD_ITEM_TO_BASKET',
id: itemId
}
}
// basket reducer
function basket(state = initialState, action) {
switch (action.type) {
case 'ADD_ITEM_TO_BASKET':
return R.append(
action.id,
state.basket.items
);
...
default:
return state;
}
}
<div class="product" ng-repeat="product in vm.products">
<product-details item="item"></product-details>
<a ng-click="vm.addItemToBasket(product.id)">
add to basket
</a>
</div>
- Async Actions?
- "Smart" Thunk actions?
- Logging?
Middlewares
import { createStore, applyMiddleware } from ‘redux’;
import rootReducer from ‘../reducers’;
import loggerMiddleware from ‘logger’;
const createStoreWithMiddleware =
applyMiddleware(loggerMiddleware)(createStore);
export default function configureStore(initialState) {
return createStoreWithMiddleware(rootReducer, initialState);
}
const store = configureStore();
Case study: Conditional action
function addItemToBasket(itemId) {
if ( userIsLoggedIn ) {
return {
type: 'ADD_ITEM_TO_BASKET',
id: itemId
}
}
else {
return {
type: 'SHOW_MESSAGE',
message: 'You have to log in to use basket."
}
}
}
Case study: Conditional action
function addItemToBasket(itemId) {
return (dispatch, getState()) {
if ( getState().user.isAuthenticated ) {
dispatch({
type: 'ADD_ITEM_TO_BASKET',
id: itemId
});
}
else {
dispatch({
type: 'SHOW_MESSAGE',
message: 'You have to log in to use basket."
});
}
}
}
const thunkMiddleware = ({ dispatch, getState }) =>
next => action =>
typeof action === 'function' ?
action(dispatch, getState) :
next(action);
Case study: API Calls
function logIn(username, password) {
return {
type: ['LOGIN_SUCCESS', 'LOGIN_REQUEST', 'LOGIN_FAILURE'],
promise: fetch('/api/login', {
method: 'post',
body: JSON.stringify({username, password})
})
};
}
const promiseMiddleware = store => next => action => {
const { promise, type: [SUCCESS, REQUEST, FAILURE], ...rest } = action;
if (!promise) return next(action);
next({ ...rest, type: REQUEST });
return promise
.then(response => {
store.dispatch({ ...rest, response, type: SUCCESS });
return true;
})
.catch(error => {
store.dispatch({ ...rest, error, type: FAILURE });
console.error(error);
return false;
});
};
Case study: Tracking
const trackingMiddleware = () => next => action => {
if(action.type === 'BUY_ITEM_CLICKED') {
myAnalyticsProvider.track('item bought', action.item.id);
}
return next(action);
};
Questions?
Redux++
By Pawel Grabarz
Redux++
The story about state
- 1,115