React Redux 里面如何正确的设计 reducer?

1272729223 · 2016年04月21日 · 最后由 1272729223 回复于 2016年04月22日 · 10311 次阅读

我感觉 reducer( state) 的设计非常伤脑经,想用combineReducers()来拆分,但是有些全局的变量又在个个子reducer的外部。

比如 我的stateisFetchingisAuthenticated显然是希望全局的,然而我想把userposts单独拿出去用子reducer写,这样一来,在userposts的异步操作中,我就没办法去更改全局的isFetching变量了?

const initState = {
    isFetching: false,
    isAuthenticated: false,
    user: {
    },
    posts: []
}

问的不是很好,大致就是这么个意思。

  1. isFetching 是一个全局的判断是否进行 http 请求的标记吗?如果是这样,那么仅仅一个 bool 不够好用。因为有时会同时发送好多请求,要等所有请求结束才,isFetching 才算结束,所以这里或许需要个数组来存储发送的请求有哪些,等请求一一返回,然后再一一删除,当数组为空,表示没有 no fetching.

  2. isAuthenticated 这个如果是一个比较大的权限,可以 获取权限后,再启动项目。

  3. 项目中的 reducer 一般分为数据相关和 UI 相关,我们的经验是数据相关以资源命名放在顶层,所以 UI 状态以 UI namespce 放在顶层 .

你可以触发多个 actions 呀~

FYI: https://github.com/reactjs/redux/issues/749

As for authentication, you should checkout HOC.

#1 楼 @small_fish__ 感谢,,关于项目中的 reducer 一般分为数据相关和 UI 相关,我们的经验是数据相关以资源命名放在顶层,所以 UI 状态以 UI namespce 放在顶层 . 可以给具体一些的例子吗?最好可以看代码,谢谢。

#2 楼 @nightire 没怎么看懂。 :(

这是我想到的一种方案,但不知道业务膨胀之后会否比较难控制。

reducers/index.js

import { assign } from "../utils"
import login from "./auth/login"
// ...

const initState = {
    isFetching: false,
    isAuthenticated: !!cookie.parse(document.cookie)["_at_"]
}

const reducerMap = {
    "LOGIN": login
    // ...
}

export default function app(state = initState, action) {
    const type = action.type
    let tmpState

    // when `"LOGIN_REQUEST"`
    if (/REQUEST/.test(type)) {
        tmpState = assign({}, state, {
            isFetching: true
        })
    }

    // when `"LOGIN_SUCCESS"` or `"LOGIN_FAILURE"`
    if (/SUCCESS|FAILURE/.test(type)) {
        tmpState = assign({}, state, {
            isFetching: false
        })
    }

    for (let prop in reducerMap) {

        // `"LOGIN_REQUEST".indexOf("LOGIN")`
        // `"LOGIN_SUCCESS".indexOf("LOGIN")`
        // `"LOGIN_FAILURE".indexOf("LOGIN")`
        if (type.indexOf(prop) > -1) {

            // return reducerMap["LOGIN"](tmpState, action)
            return reducerMap[prop](tmpState, action)
        }
    }

    // default
    return state
}

actions/auth/login.js

import {
    LOGIN_SUCCESS,
    LOGIN_FAILURE
} from "../../constants/actionTypes"
import { assign } from "../../utils"

export default function login(state, { type, payload }) {
    if (type === LOGIN_SUCCESS) {
        return assign({}, state, {
            isAuthenticated: true,
            message: payload.message
        })
    }

    if (type === LOGIN_FAILURE) {
        return assign({}, state, {
            message: payload.message
        })
    }
}

constants/actionTypes.js

// LOGIN
export const LOGIN_REQUEST = "LOGIN_REQUEST"
export const LOGIN_SUCCESS = "LOGIN_SUCCESS"
export const LOGIN_FAILURE = "LOGIN_FAILURE"

#5 楼 @1272729223 我就不多说了,看看这个东西的文档:https://github.com/pburtchaell/redux-promise-middleware/tree/master/docs ,特别是针对你提的问题,专门看一下:https://github.com/pburtchaell/redux-promise-middleware/blob/master/docs%2Fguides%2Fglobal-handling.md 。不过这一章的解决方案是基于该中间件自身的,所以要彻底理解其中的运作过程,还是通读一遍为好。

基本的概念是:async action,这里以 promise 为例,总是会有特定数目的未来状态需要处理的。这些状态会涉及到 UI 状态的变化,也会涉及到 Data 状态的变化。最 raw 的方案就是若干个 actions 分别处理不同的未来状态然后连起来用。比如说:

dispatch({type: '全局/加载', payload: true});  // action to handle loading notification
API.fetch(url)
  .then(response => response.json())
  .then(payload => dispatch({type: '数据/入库', payload}))  // action to transfer data into corresponding reducer
  .then(() => dispatch({type: '全局/加载', payload: false}))  // action to turn loading state off
  .catch(error => dispatch({type: '全局/错误', payload: error});  // action to handle possible error(s)

然而,这种“模式”是可以预期的(promise 总是这样的),所以你可以构造中间件(比如前面提到的那个)来抽象这件事情,于是你就可以统一来处理这种情形并且保证 UI 状态和 Data 状态是解耦的,而且它们各自也是很容易复用的了。


顺便多插一句,针对前面说的多个 async actions 共用 loading 的场景,其实不用那么麻烦,完全可以:

dispatch({type: '全局/加载', payload: true});

Promise.all([
  API.fetch(url_1),
  API.fetch(url_2),
  ...
])
  .then(responses => responses.map(response => response.json()))
  .then([payload_1, payload_2, ...] => {
    // fire off corresponding actions
    // ...
  })
  .then(() => dispatch({type: '全局/加载', payload: false}))  // action to turn loading state off
  .catch(error => dispatch({type: '全局/错误', payload: error});  // action to handle possible error(s)

这些只是 raw redux 的用法,善加利用一些简单的中间件,比如 redux-thunk / redux-promise 之类的,上述代码可以精简到非常可观的地步。要善于捕捉重复行为之中的共通模式,然后利用好 redux 的各种机制(主要是 middleware)


不是一定要用 redux-promise-middleware 或是其他类似的中间件,不过这个文档里倒是把 async action 的许多细节都解释的很清楚,你可以视情况自己选择一些解决方案。

我理解很多人做应用程序架构的时候都倾向于越少依赖越好,主要原因是增加抽象层次会带来学习和理解的负担。不过增加抽象层次的目的就是把解决一些复杂且琐碎的事情的过程封装起来并提供简明清楚的 API。所以如果你是负责架构的人,你就需要多多阅读周边的一些东西(在这里指的是 redux 周边),其实它们大多都是一些中间件,理解了这些中间件处理的过程,你也可以不依赖这些周边,或者是对它们胸有成竹随便用哪个也无所谓。

#8 楼 @nightire 好的,太感谢了。太感动了。好人好报。

#8 楼 @nightire Promise.all是个好东西。:D 这样基本可以解决建立一个fetchQueue的队列了,然后一个个检查fetch结果。

#8 楼 @nightire 刚才仔细再看了一遍,你的这种 raw 方式就是我想要的。不过我也是把 fetch 再用 promise 封装了一层。只是少了个全局处理的 action。感谢。

简单的说,在 action creator 里面 dispatch 其他的 action 去触发额外的状态变更。这需要 action creator 不止返回一个 object,Redux 的 thunk 和 promise 等 middleware 都支持这种做法。说的比较抽象,不过跟 @nightire 说的是一个意思。

#12 楼 @darkbaby123 #8 楼 @nightire

刚才稍微改了下我的方案,似乎也可以。假设,截获所有REQUEST|SUCCESS|FAILURE的动作,然后根据前缀使用制定的reducer。见上面。

14 楼 已删除
需要 登录 后方可回复, 如果你还没有账号请 注册新账号