Jakub Chodorowicz
Young/Skilled
@chodorowicz
github.com/chodorowicz
- Why to do it
- Benefits
- Downsides
- Start state with Alt
- First take ➡️ pure Redux
- Redux actions
- React redux
- Async with Redux
- Pure
- Thunk
- Sagas
- hype & trend ✨
- forces FP & immutability
- community
- popularity (more developers who know it)
- better dev tools
- testability
- occasion to go through all of your code base
- clearer API / flow
- time
- bugs (tests help here)
stores
-- UiStore
-- OrderStore
-- ...
actions
-- UiActions
-- OrderActions
-- ...
components
-- ...
Store
import alt from '../alt';
import UiActions from '../actions/UiActions';
import OrderActions from '../actions/OrderActions';
class UiStore {
constructor() {
this.isMenuOpened = false;
this.isSaving = false;
// ...
}
this.bindActions(UiActions);
this.bindActions(OrderActions);
this.exportPublicMethods({
getIsMenuOpened: () => this.isMenuOpened,
// ...
});
}
export default alt.createStore(UiStore, 'UiStore');
Actions
import alt from '../alt';
import axios from 'axios';
class UiActions {
constructor() {
this.generateActions(
'toggleMenu',
'toggleCreateModal',
// ...
);
}
}
export default alt.createActions(UiActions);
// component.jsx
import React from 'react';
import UiStore from '../../stores/UiStore';
import cx from 'classnames';
import UiActions from '../../actions/UiActions';
let MenuToggle = React.createClass({
getInitialState() {
return {
isMenuOpened: UiStore.getIsMenuOpened(),
};
},
componentDidMount() {
UiStore.listen(this.onChange);
},
componentWillUnmount() {
UiStore.unlisten(this.onChange);
},
toggleMenu() {
UiActions.toggleMenu();
},
onChange() {
this.setState(this.getInitialState());
},
render: function() {
let classes = cx({
'icon': true,
'icon-icon_hamburger': !this.state.isMenuOpened,
'icon-close_black': this.state.isMenuOpened,
});
return (
<div onClick={this.toggleMenu} className="MenuToggle">
<span className={classes}></span>
</div>
);
},
});
export default MenuToggle;
- predictable state container for JavaScript apps
- support: logging, hot reloading, time travel, universal apps, record and replay
- single state object
// store.js
import update from 'react-addons-update';
const initialUiState = {
isMenuOpened: false,
isCreateModalVisible: false,
isSaving: false,
// ...
};
const initialState = {
ui: initialUiState,
rest: {},
};
const rest = (state = {}, action) => {
return state;
};
const ui = (state = {}, action) => {
switch(action.type) {
case 'TOGGLE_MENU':
return update(state, {isMenuOpened: {$set: !state.isMenuOpened}});
case 'TOGGLE_CREATE_MODAL':
let newState = update(state, {isCreateModalVisible: {$set: action.isCreateModalVisible}});
if(action.isCreateModalVisible) {
newState = update(state, {isSelectModalVisible: {$set: false}});
}
return newState;
default:
return state;
}
return state;
};
const rootReducer = combineReducers({
ui,
rest,
});
const store = createStore(
rootReducer,
initialState,
window.devToolsExtension ? window.devToolsExtension() : undefined
);
// component.jsx
import React from 'react';
// import UiStore from '../../stores/UiStore';
import store from 'js/store/store';
import cx from 'classnames';
// import UiActions from '../../actions/UiActions';
let MenuToggle = React.createClass({
getInitialState() {
return {
isMenuOpened: store.getState().ui.isMenuOpened,
};
},
componentDidMount() {
// UiStore.listen(this.onChange);
this.unsusbscribe = store.subscribe(this.onChange);
},
componentWillUnmount() {
this.unsubscribe();
// UiStore.unlisten(this.onChange);
},
toggleMenu() {
store.dispatch({
type: 'TOGGLE_MENU',
});
// UiActions.toggleMenu();
},
onChange() {
this.setState(this.getInitialState());
},
render: function() {
// ...
return (
<div onClick={this.toggleMenu} className="MenuToggle">
<span className={classes}></span>
</div>
);
},
});
export default MenuToggle;
- helps to define actions in a more cleaner manner
- helps to follow FSA
- helps to define reducers in a cleaner way
- https://github.com/acdlite/redux-actions
actions.js before
export const TOGGLE_MENU = 'TOGGLE_MENU';
// ...
export function toggleMenu(isOpen) {
return {type: TOGGLE_MENU, isOpen};
}
export function toggleCreateModal(isOpen) {
return {type: TOGGLE_CREATE_MODAL, isOpen};
}
// actions.js after
import {createAction} from 'redux-actions';
export const TOGGLE_MENU = 'TOGGLE_MENU';
export const TOGGLE_CREATE_MODAL = 'TOGGLE_CREATE_MODAL';
export const toggleMenu = createAction(TOGGLE_MENU);
export const toggleCreateModal = createAction(TOGGLE_CREATE_MODAL);
-
should be really called
createActionCreator
-
follows FSA - Flux Standard Action
-
An action MUST
- be a plain JavaScript object.
- have a type property.
-
An action MAY
- have a error property.
- have a payload property.
- have a meta property.
{
type: 'ADD_TODO',
payload: {
text: 'Do something.'
}
}
handleActions
creates a reducer (function which takes state and action and returns new state)
// uiReducer.js
import {handleActions} from 'redux-actions';
import update from 'react-addons-update';
import {
TOGGLE_MENU,
// ...
} from '/actions/actions.js';
const initialState = {
isMenuOpened: false,
// ...
};
const toggleMenu = (state, action) => update(state, {isMenuOpened: {$set: !state.isMenuOpened}});
const toggleSelectModal = (state, action) => {
return update(state, {
isSelectModalVisible: {$set: !state.isSelectModalVisible},
isScreenBlockerVisible: {$set: !state.isSelectModalVisible},
});
};
const ui = handleActions({
TOGGLE_MENU: toggleMenu,
TOGGLE_CREATE_MODAL: toggleCreateModal,
// ...
}, initialState);
export default ui;
-
very simple API (just
<Provider>
compontent andconnect
method) but very useful -
help to clearly defined presentational and container components
-
removes a lot of boilerplate
-
speed improvements
-
<Provider>
container component makes Redux store available to all child components
// main.jsx
import {Provider} from 'react-redux';
import store from 'js/store/store';
import React from 'react';
import ReactDOM from 'react-dom';
import AppRouter from 'js/components/AppRouter';
ReactDOM.render(
<Provider store={store}><AppRouter /></Provider>,
document.getElementById('app')
);
- gets store through context of
<Provider>
- provides rendering optimisation through
shouldComponentUpdate
and shallow comparison of own props and store props
// component.jsx
import React from 'react';
import cx from 'classnames';
import {toggleMenu} from 'js/actions/ReduxActions';
import {connect} from 'react-redux';
const MenuToggle = ({isMenuOpened, handleToggleMenu}) => {
const classes = cx({
'icon': true,
'icon-icon_hamburger': !isMenuOpened,
'icon-close_black': isMenuOpened,
});
return (
<div onClick={handleToggleMenu} className="MenuToggle">
<span className={classes}></span>
</div>
);
};
const mapStateToProps = (state, ownProps) => {
return {isMenuOpened: state.ui.isMenuOpened};
};
const mapDispatchToProps = (dispatch) => {
return {
handleToggleMenu: () => { dispatch(toggleMenu()); },
};
};
export default connect(mapStateToProps, mapDispatchToProps)(MenuToggle);
// action creator
function loadData(dispatch, userId) { // needs to dispatch, so it is first argument
return fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
);
}
// component
componentWillMount() {
loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch
}
- extracting all async functionality to separate modules forces us to pass dispatch as action parameter
- if
store
was imported as singleton it would break FP flow and server side rendering
- if
- we need to know which actions are async and pass dispatch to them
- middleware which allows dispatching actions
- components don't need to know if actions is async or not
// action creator
function loadData(userId) {
return dispatch => {
dispatch(type: 'LOAD_DATA'});
fetch(`http://data.com/${userId}`) // Redux Thunk handles these
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
);
};
}
// component
componentWillMount() {
this.props.dispatch(loadData(this.props.userId)); // dispatch like you usually do
}
- but in our case we still need to pass dispatch around to other components
// api.js
function getConfiguration(dispatch) {
return axios.get(`${api}configuration`, {
headers: setTokenHeader(UserStore.getToken()),
})
.then( (response) => {
UiActions.getConfigurationSuccess(response.data);
if(dispatch) {
dispatch({type: 'FETCH_CONFIGURATION_SUCCESS', data: response.data});
}
}).catch(catchHandler);
}
- long-live processes that interacts with the system by:
- Reacting to actions dispatched in the system.
- Dispatches new actions into the system.
- Can "wake itself" using internal mechanisms without actions being dispatched. e.g. waking up on interval
- In redux-saga, a saga is a generator function that can run indefinitely inside the system. It can be woken up when a specific action is dispatched. It can dispatch additional actions, and has access to the application state atom.
export const fetchConfigurationRequest = createAction(FETCH_CONFIGURATION_REQUEST);
export const fetchConfigurationSuccess = createAction(FETCH_CONFIGURATION_SUCCESS);
export const fetchConfigurationSuccess = createAction(FETCH_CONFIGURATION_FAILURE);
import { takeEvery, takeLatest } from 'redux-saga';
import { call, put, take, fork } from 'redux-saga/effects';
import api from 'js/api';
import * as actions from 'js/actions/ReduxActions';
function* fetchConfiguration(action) {
try {
const data = yield call(api.general.getConfiguration);
yield put({type: actions.FETCH_CONFIGURATION_SUCCESS, data: data});
} catch (e) {
yield put({type: actions.FETCH_CONFIGURATION_FAILURE, message: e.message});
}
}
export default function* rootSaga() {
yield* takeEvery(actions.FETCH_CONFIGURATION_REQUEST, fetchConfiguration);
}
// reducer.js
const handleFetchConfigurationSuccess = (state, action) => {
// ...
return newState;
};
const config = handleActions({
FETCH_CONFIGURATION_REQUEST: (state, action) => state,
FETCH_CONFIGURATION_SUCCESS: handleFetchConfigurationSuccess,
FETCH_CONFIGURATION_SUCCESS: handleFetchConfigurationFailure,
}, initialState);