The story about state
UI
Filters
Search Results
Sorting
UI
UI
Tracking
Log
Validation
Locale
Tabs
Super crazy requirement
Something happened
State changed
UI is rendered
UI is rendered
Something happened
State changed
UI is rendered
UNI-DIRECTIONAL
DATA FLOW
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)
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
// 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}
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
{
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
}
}
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
}
)
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
Time Traveling
+
Hot Module Reloading
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:
{
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>
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);
};