Skip to content

Instantly share code, notes, and snippets.

@chodorowicz
Last active February 9, 2017 00:41
Show Gist options
  • Save chodorowicz/c4916dbe1c81ccb14caf to your computer and use it in GitHub Desktop.
Save chodorowicz/c4916dbe1c81ccb14caf to your computer and use it in GitHub Desktop.
From Alt to Redux ecosystem (redux-actions, redux-thunk, react-redux, redux-saga)

From Alt to Redux ecosystem (redux-actions, redux-thunk, react-redux, redux-saga)

Jakub Chodorowicz

Young/Skilled

@chodorowicz
github.com/chodorowicz
  1. Why to do it
  2. Benefits
  • Downsides
  1. Start state with Alt
  2. First take ➡️ pure Redux
  3. Redux actions
  4. React redux
  5. Async with Redux
  6. Pure
  7. Thunk
  8. Sagas

Why to do it

Benefits

  • 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

Downsides

  • time
  • bugs (tests help here)

Initial state

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;

Pure redux

  • 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;

Redux actions

Redux actions - actions.js

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.'  
  }
}

Redux actions - reducers

  • 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;

React redux

  • very simple API (just <Provider> compontent and connect 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);

Async with Redux

// 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
  • we need to know which actions are async and pass dispatch to them

Async with redux-thunk

  • 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);
}

Async with redux-saga

  • 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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment