Notes On Redux

Table of Contents

References

Overview

  • It helps to maintain a global state container.
  • It is independent of Redux and other UI frameworks. It works with them all well.
  • Helps to track application state history (and easy to implement undo/redo logging)
  • The react-redux is a react binding for redux.
  • You can simply use redux in html using redux javascipt library.
  • The Redux DevTools make it easy to trace when, where, why, and how your application's state changed.

Core Concepts

  • Global Store, Event Listener, Dispatcher, Actions, Subscribe to store.

  • Reducer - function(state, action) { return modified state; } :

    export default function(state = {}, action) {
       switch (action.type) {
         case DELETE_POST:
           return _.omit(state, action.payload);    
        ...
       }
    }
    
  • A combined reducer can handle any action by passing it through all.

  • Combined reducer creates separate name space with in global state :

    postReducer = Operates on state to change any attribute. e.g. state.maxPosts = 10;
    newsReducer = Operates on state to change any attribute. e.g. state.total    = 20;
    
    netReducer = combineReducers( { posts : postReducer, news : newsReducer } )
         Operates on state.posts and state.news only.
    Note that the combined reducer never messes up common state.
    
  • Dispatcher invokes the (combined) reducer and then all listeners. Each listener is a plain function without any args.

  • A store takes a (combined) reducer and initial state and gives you these handy little functions for you. You dont have to write these functions:

    • dispatch : Dispatch function. You don't have to write one.
    • subscribe: Call this to add listener. e.g. render method.
    • getState: Get current State.
    • replaceReducer: You can supply a new reducer later as well.

React Redux Concepts

  • React-Redux provides following artificats: Provider, connect(), batch() and Hooks.

Synopsis :

ReactDOM.render(
     <Provider store={store}>
       <App />
     </Provider>,
     document.getElementById('root')
)

//
// With Router ...
//
import { Router, Route } from 'react-router-dom'

ReactDOM.render(
   <Provider store={store}>
     <Router history={history}>
       <Route exact path="/" component={App} />
       <Route path="/foo" component={Foo} />
       <Route path="/bar" component={Bar} />
     </Router>
   </Provider>,
   document.getElementById('root')
)

Provider

  • Makes the Redux store available to any nested components.
  • The nested components must have been wrapped in the connect() function.

connect()

  • Top level wrapper component called Provider is associated with a Redux Store.
  • A react redux router enables fetching global state even before Router is invoked.
  • Each React component already has concept of local "State". Redux state can complement this with global state.

FAQ

How to automate re-render of App on need basis from child component ?

  • The render function as a listener can be subscribed to global redux store. Then each time a global state changes, the component can be redrawn. That will be too many redraws.

  • Another option is to use reducer and dispatch action from child. To define such reducer, you need a handle to render function early on.

  • Typically reducer is used to alter global state. If all you want to do is to trigger action function, you can just remember the function handle in global state: e.g. :

    store.getState().redrawAppFunc = renderApp;   // Not recommended.
    // Recommended to implement set logic in reducer ...
    store.dispatch( { type: SET_REDRAW_FUNC, payload: renderApp })
    // Call render on need basis from child ...
    store.getState().redrawAppFunc();
    

Appendix: Slim Redux code

// Helper function which transforms Object.
function mapValues(obj, fn) {
  return Object.keys(obj).reduce((result, key) => {
    result[key] = fn(obj[key], key);
    return result;
  }, {});
}

// Helper function to select subset of obj attributes using boolean func.
function pick(obj, fn) {
  return Object.keys(obj).reduce((result, key) => {
    if (fn(obj[key])) {
      result[key] = obj[key];
    }
    return result;
  }, {});
}


// Simple function compose -- Applied to auto bind the dispatch with Action
function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args));
}

export function bindActionCreators(actionCreators, dispatch) {
  return typeof actionCreators === 'function' ?
    bindActionCreator(actionCreators, dispatch) :
    mapValues(actionCreators, actionCreator =>
      bindActionCreator(actionCreator, dispatch)
    );
}

// Given a list of functions, this returns single composed function.
export function compose(...funcs) {
  return arg => funcs.reduceRight((composed, f) => f(composed), arg);
}


export function applyMiddleware(...middlewares) {
  return (next) => (reducer, initialState) => {
    var store = next(reducer, initialState);
    var dispatch = store.dispatch;
    var chain = [];
    chain = middlewares.map(middleware => middleware({
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }));
    dispatch = compose(...chain)(store.dispatch);
    return {...store,
      dispatch
    };
  };
}

// Combined reducer can handle any action and return modified state!
export function combineReducers(reducers) {
  var finalReducers = pick(reducers, (val) => typeof val === 'function');
  return (state = {}, action) => mapValues(finalReducers,
    (reducer, key) => reducer(state[key], action)
  );
}

//
// The crux of the code.
// Input:    A combined reducer, initialState
// Output: 
//  dispatch,
//  subscribe,
//  getState,
//  replaceReducer
//
export function createStore(reducer, initialState) {
  var currentReducer = reducer;
  var currentState = initialState;
  var listeners = [];
  var isDispatching = false;
  function getState() {
    return currentState;
  }

  //
  // A listener is a simple function without args.
  // It will be called once for each dispatch of any action!
  //
  function subscribe(listener) {
    listeners.push(listener);
    return function unsubscribe() {
      var index = listeners.indexOf(listener);
      listeners.splice(index, 1);
    };
  }

  //
  // Compute current state; Then also call all listeners.
  // Current reducer is a kind of listener. But you also have plain listeners.
  //
  function dispatch(action) {
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.');
    }
    try {
      isDispatching = true;
      currentState = currentReducer(currentState, action);
    } finally {
      isDispatching = false;
    }
    listeners.slice().forEach(listener => listener());
    return action;
  }
  //
  //  Replace current reducer with new one.
  //
  function replaceReducer(nextReducer) {
    currentReducer = nextReducer;
    dispatch({
      type: '@@redux/INIT'     // Dummy action to force call all reducers again.
    });
  }
  dispatch({
    type: '@@redux/INIT'       // Dummy Action: Useful to compute currentState.
  });
  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer
  };
}

Appendix: React-Redux connect.js

// connect() is a function that injects Redux-related props into your component.
// You can inject data and callbacks that change that data by dispatching actions.

function connect(mapStateToProps, mapDispatchToProps) {

  // It lets us inject component as the last step so people can use it as a decorator.
  // Generally you don't need to worry about it.

  return function (WrappedComponent) {

    // It returns a component
    return class extends React.Component {

      render() {
        return (
          // that renders your component
          <WrappedComponent
            {/* with its props  */}
            {...this.props}
            {/* and additional props calculated from Redux store */}
            {...mapStateToProps(store.getState(), this.props)}
            {...mapDispatchToProps(store.dispatch, this.props)}
          />
        )
      }

      componentDidMount() {
        // it remembers to subscribe to the store so it doesn't miss updates
        this.unsubscribe = store.subscribe(this.handleChange.bind(this))
      }

      componentWillUnmount() {
        // and unsubscribe later
        this.unsubscribe()
      }

      handleChange() {
        // and whenever the store state changes, it re-renders.
        this.forceUpdate()
      }
    }
  }
}

// This is not the real implementation but a mental model.
// It skips the question of where we get the "store" from 
//   (answer: <Provider> puts it in React context)

// The purpose of connect() is that you don't have to think about
// subscribing to the store or perf optimizations yourself, and
// instead you can specify how to get props based on Redux store state:

const ConnectedCounter = connect(

  // Given Redux state, return props
  state => ({
    value: state.counter,
  }),

  // Given Redux dispatch, return callback props
  dispatch => ({
    onIncrement() {
      dispatch({ type: 'INCREMENT' })
    }
  })
)(Counter)