Redux Internals

Vladimir Novick

mail: vnovick@gmail.com

twitter: @VladimirNovick

github: vnovick

facebook: vnovickdev

Frontend Developer & Architect

Redux

Agenda

  • Redux in a nutshell

  • Middleware signature

  • Enhancing store

  • Context wormholes

Redux in a nutshell

Is Flux looks like this?

or...

Well Redux is

So Why Redux?

  • Separation of concerns

  • FP driven

  • Awesome developer experience

Three Principles

  • Single source of truth

  • State is changed only by emitting an action

  • Mutations described by pure functions (reducers)

The data flow

Middleware signatures

Middleware is the suggested way to extend Redux with custom functionality. Middleware lets you wrap the store’s dispatch method for fun and profit

Middleware is a dispatch wrapper

import createLogger from 'redux-logger'
import { createStore } from 'redux'

let store = createStore(rootReducer,
    applyMiddleware(thunk, createLogger())
)

Logger

Signature

store => next => action

Middleware

const applyStateTransform = (action) => {
  let { transform, state } = action;
  return transform ? transform(state) || state : state
}

const fetcher = store => next => action => {
  if (action.type === '@@fetcher/FETCH'){
    let { url, method, resolve, reject, error } = action.state;
    fetch(url, {
      method
    }).then((result)=> {
      let resolveAction = resolve(result);
      let errorAction = error(result);
      return result.ok ?
        next({
          type: `@@fetcher/${resolveAction.type}`,
          state: applyStateTransform(resolveAction)
        }) :
        next({
          type: `@@fetcher/${errorAction.type}`,
          state: applyStateTransform(errorAction)
        })
    }).catch((err) => {
      let rejectAction = reject(err);
      return next({
        type: `@@fetcher/${rejectAction.type}`,
        state: applyStateTransform(rejectAction)
      })
    });
  }
  return next(action)
}


////Fetch Action Creator


export const fetchFromServer = (url, method) => dispatch => {
  dispatch({
    type: '@@fetcher/FETCH',
    state: {
      url,
      method,
      resolve: result => {
        return {
          type: 'FETCH_SUCCESS',
          state: result
        }
      },
      reject: err => {
        return {
          type: 'FETCH_ERROR',
          state: err
        }
      },
      error: err => {
        return {
          type: 'RESPONSE_ERROR',
          state: err,
          transform: ()=>{
            dispatch({
              type: 'Lalala'
            })
          }
        }
      }
    }
  })
}

Custom fetch middleware

Enhancing a store

Signature

createStore => createStore

// Usage

componentDidMount(){
    let { dispatch } = this.props;
    dispatch(inject('player', this))
    console.log(appStore.getState(this))
}

//Result will be player stateKey value

//Inject Action Creator

export const inject = (state, component) => {
  return {
    type: '@@redux/INJECT',
    state
    component
  }
}

function diStoreEnhancer(){
  return createStore => (...args) => {
    const store = createStore(...args);
    const diContainer = []
    const dispatch = function decoratedDispatch(action){
      if (action.type === '@@redux/INJECT') {
        const { component, state } = action;
        diContainer.push({ component, stateKey: state })
      }
      return store.dispatch(action)
    }
    const getState = function decoratedState() {
      if (component) {
        return store.getState()[diContainer
            .filter(di => di.component === component)
            .reduce( (prev,next) => next, {})
            .stateKey
        ];
      }
      return store.getState()
    }
    return {
      ...store,
      ...{
        dispatch,
        getState
      }
    };
  };
}



Compose

import { compose } from 'redux'

const createComposedStore = compose(
  storeEnhancer(),
  applyMiddleware(fetcher, createLogger()
)(createStore);

export default createComposedStore(rootReducer);

Context wormholes

Container(Smart) Component: 

Specifies data and behavior for component but does not know anything about appearance

 

Presentational(Dumb) Component: 

Specify appearance of component but knows nothing about behavior

React Components 

Passing Store techniques

Passing explicitly via props

The problem: 

If you have a component tree, store must be passed through props for all components in the tree.

Passing Store techniques

The solution: 

Only container components get store from context when defining contextTypes.

Passing implicitly via Context

<Provider store={store}>

Context

connect(...fns)(component)

React Redux

Provider

import { Provider } from 'react-redux'
import { appStore} from 'appStore'

<Provider store={ appStore }>
  <PlayerContainer/> 
</Provider>

connect(...fns)(component)

import * from 'actionCreators';
import { connect } from 'react-redux';
// ...
// ...Player React Component definition

function mapStateToProps(state){
  const { src, poster, showControls } = state.player;
  return { 
    src, poster, showControls
  }
}

export const PlayerContainer = connect(
    mapStateToProps
)(Player);

Passing around actions

import React from 'react';
import * from 'actionCreators';
import { connect } from 'react-redux';

class Player extends React.Component { 

  play(){
    this.props.dispatch(actionCreators.play())
  }

  render() {
    return (
        <PlayerWrapper>
            <PlayerSurface/>
            <PlayerControls play={this.play.bind(this}/>
        </PlayerWrapper>
    )    
  }
}

//export PlayerContainer with connect as in previous slide

mapDispatchToProps

import React from 'react';
import * from 'actionCreators';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

class Player extends React.Component { 

  render() {
    return (
        <PlayerWrapper>
            <PlayerSurface/>
            <PlayerControls play={this.props.actions.play} />
        </PlayerWrapper>
    )    
  }
}

function mapStateToProps(state){
//...same as previous slide
}

function mapDispatchToProps(dispatch){
  return {
    actions: bindActionCreators(actionCreators, dispatch)
  }
}


export const PlayerContainer = connect(
    mapStateToProps,
    mapDispatchToProps
)(Player);

React-Redux summary

  • Provider:  wraps root component and makes possible to use connect() 

  • connect:  executed on every state change if specified. returned object is merged into component props

  • mapStateToProps: executed on every state change if specified. returned object is merged into component props
  • mapDispatchToProps: executed on every state change, given dispatch as parameter. returned object will be injected into props. Mostly used together with bindActionCreators

react@90min.com

Redux Internals

By vladimirnovick

Redux Internals

ReactJS Meetup #5 @Facebook

  • 6,627