1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

手っ取り早くReduxがどのようなものか理解する~middleware, combineReducers編~

Last updated at Posted at 2016-04-24

middleware

applyMiddlewareで設定している。
https://github.com/reactjs/redux/blob/master/src/applyMiddleware.js

applyMiddleware
export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, initialState, enhancer) => {
    var store = createStore(reducer, initialState, enhancer)
    var dispatch = store.dispatch
    var chain = []

    var middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

この通り、createStoreを引数にして、新しいstoreとdispatchを返すfunctionを返しています。(curry化が可能)
middlewareは、middlewareAPIを引数としなければならないことがわかる。

composeがどのようなことを行っているかを見てみる。

compose.js
export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  } else {
    const last = funcs[funcs.length - 1]
    const rest = funcs.slice(0, -1)
    return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
  }
}

compose()(hoge)のように引数がない場合は、hogeだけが返される。
compose(hoge, foo, bar)(something)のように引数がある場合は、last(...args)を初期値としてreduce(hoge(foo(bar(something))))しています。
f = middleware(middlewareAPI)となり、composed = f(composed)となります。
よくある、middlewareでwrapし続ける処理ですね。

combineReducers

combineReducersで設定している。
https://github.com/reactjs/redux/blob/master/src/combineReducers.js

combineReducers
export default function combineReducers(reducers) {
  var reducerKeys = Object.keys(reducers)
  var finalReducers = {}
  for (var i = 0; i < reducerKeys.length; i++) {
    var key = reducerKeys[i]
    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  var finalReducerKeys = Object.keys(finalReducers)

  var sanityError
  try {
    assertReducerSanity(finalReducers)
  } catch (e) {
    sanityError = e
  }

  return function combination(state = {}, action) {
    if (sanityError) {
      throw sanityError
    }

    if (process.env.NODE_ENV !== 'production') {
      var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action)
      if (warningMessage) {
        warning(warningMessage)
      }
    }

    var hasChanged = false
    var nextState = {}
    for (var i = 0; i < finalReducerKeys.length; i++) {
      var key = finalReducerKeys[i]
      var reducer = finalReducers[key]
      var previousStateForKey = state[key]
      var nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === 'undefined') {
        var errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }
}

複数のreducerを引数にとり、combinationされたreducerを返す。
基本、stateactionを引数に取るfunctionはreducerと考えて良い。
ひとつずつ処理を見ていく。

var reducerKeys = Object.keys(reducers)
Object.keysは列挙可能なpropertyしか返さないので、reducersは{ hoge: (state, action) => ..., foo: (state, action) => ..., ...}のような形式でなければならない。

finalReducers[key] = reducers[key]
functionであるreducerのみがfinalReducersに登録される。

var finalReducerKeys = Object.keys(finalReducers)
もう一度keyだけ取り出す。

assertReducerSanity(finalReducers)
assertReducerSanityを見てみる。

asserReducerSanity
function assertReducerSanity(reducers) {
  Object.keys(reducers).forEach(key => {
    var reducer = reducers[key]
    var initialState = reducer(undefined, { type: ActionTypes.INIT })

    if (typeof initialState === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined during initialization. ` +
        `If the state passed to the reducer is undefined, you must ` +
        `explicitly return the initial state. The initial state may ` +
        `not be undefined.`
      )
    }

    var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.')
    if (typeof reducer(undefined, { type }) === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined when probed with a random type. ` +
        `Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` +
        `namespace. They are considered private. Instead, you must return the ` +
        `current state for any unknown actions, unless it is undefined, ` +
        `in which case you must return the initial state, regardless of the ` +
        `action type. The initial state may not be undefined.`
      )
    }
  })
}

この通り、reducerはINIT及び定義されていないACTIONに対してinitialStateを返さないとエラーが返ります。
var reducer = (state = initialState, action) => { ... }のような形式でinitialStateに設定しなければなりません。

  ...,

  return function combination(state = {}, action) {
    var hasChanged = false
    var nextState = {}
    for (var i = 0; i < finalReducerKeys.length; i++) {
      var key = finalReducerKeys[i]
      var reducer = finalReducers[key]
      var previousStateForKey = state[key]
      var nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === 'undefined') {
        var errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }
}

var nextStateForKey = reducer(previousStateForKey, action)
前のstateから新しいstateを生成します。undefinedの場合はエラーです。

nextState[key] = nextStateForKey
reducerの名前に対応した新しいstateが登録されます。

return hasChanged ? nextState : state
渡されたreducerdでひとつでもstateが更新されていればnextStateが返され、なにも更新されなければ、渡されたstateが返されます。

code例

example
const { combineReducers } = require('redux')

const reducer1 = (state = 0, action) => {
  if (action === 'UPDATE') {
    return state + 1
  } else {
    return state
  }
}

const reducer2 = (state = 'Hello', action) => {
  if (action === 'UPDATE') {
    return state + ', world!'
  } else {
    return state
  }
}

const reducer3 = (state = {}, action) => {
  if (action === 'UPDATE') {
    return {
      a: 'this is a',
      b: 'this is b',
    }
  } else {
    return state
  }
}

const combines = combineReducers({ r1: reducer1, r2: reducer2, r3: reducer3 })

const nextStates = combines({}, 'UPDATE')

console.log(nextStates.r1) // 1
console.log(nextStates.r2) // Hello, world!
console.log(nextStates.r3.a)  // this is a
console.log(nextStates.r3.b)  // this is b

react-redux編に続く・・・(完)

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?