使用通用动作和减速器简化 redux

Posted

技术标签:

【中文标题】使用通用动作和减速器简化 redux【英文标题】:simplify redux with generic action & reducer 【发布时间】:2018-11-06 03:56:24 【问题描述】:

在 React-Redux 项目中,人们通常为每个连接的组件创建多个操作和缩减器。但是,这会为简单的数据更新创建大量代码。

为了简化和加快应用开发,使用单个通用动作和reducer来封装所有数据更改是否是一种好习惯。

使用这种方法会有什么缺点或性能损失。因为我认为没有明显的权衡,而且它使开发更加容易,我们可以将它们全部放在一个文件中!这种架构的示例:

// Say we're in user.js, User page

// state
var initialState = ;

// generic action --> we only need to write ONE DISPATCHER
function setState(obj)
    Store.dispatch( type: 'SET_USER', data: obj );


// generic reducer --> we only need to write ONE ACTION REDUCER
function userReducer = function(state = initialState, action)
    switch (action.type) 
        case 'SET_USER': return  ...state, ...action.data ;
        default: return state;
    
;

// define component
var User = React.createClass(
    render: function()
        // Here's the magic...
        // We can just call the generic setState() to update any data.
        // No need to create separate dispatchers and reducers, 
        // thus greatly simplifying and fasten app development.
        return [
            <div onClick=() => setState( someField: 1 )/>,
            <div onClick=() => setState( someOtherField: 2, randomField: 3 )/>,
            <div onClick=() => setState( orJustAnything: [1,2,3] )/>
        ]
    
);

// register component for data update
function mapStateToProps(state)
    return  ...state.user ;


export default connect(mapStateToProps)(User);

编辑

所以典型的 Redux 架构建议创建:

    包含所有操作的集中文件 包含所有减速器的集中文件

问题是,为什么是两步流程?这是另一个架构建议:

创建1 组文件,其中包含处理所有数据更改的所有setXField()。而其他组件只是使用它们来触发更改。简单。示例:

/** UserAPI.js
  * Containing all methods for User.
  * Other components can just call them.
  */

// state
var initialState = ;

// generic action
function setState(obj)
    Store.dispatch( type: 'SET_USER', data: obj );


// generic reducer 
function userReducer = function(state = initialState, action)
    switch (action.type) 
        case 'SET_USER': return  ...state, ...action.data ;
        default: return state;
    
;


// API that we export
let UserAPI = ;

// set user name
UserAPI.setName = function(name)
    $.post('/user/name',  name , function( ajaxSuccess )
        if (ajaxSuccess) setState( name );
    );
;

// set user picture URL
UserAPI.setPicture = function(url)
    $.post('/user/picture',  url , function( ajaxSuccess )
        if (ajaxSuccess) setState( url );
    );
;

// logout, clear user
UserAPI.logout = function()
    $.post('/logout', , function()
        setState(initialState);
    );
;

// Etc, you got the idea...
// Moreover, you can add a bunch of other User related methods, 
// like some helper methods unrelated to Redux, or Ajax getters. 
// Now you have everything related to User available in a single file! 
// It becomes much easier to read through and understand.

// Finally, you can export a single UserAPI object, so other 
// components only need to import it once. 
export default UserAPI

请通读上面代码部分中的 cmets。

现在不再有一堆动作/调度程序/减速器。您有 1 个文件,其中包含用户概念所需的所有内容为什么这是一种不好的做法? IMO,它让程序员的生活轻松多了,其他程序员只需从上到下阅读文件即可了解业务逻辑,无需切换回来在 action/reducer 文件之间来回切换。哎呀,甚至不需要redux-thunk!您甚至可以一一测试功能。所以可测试性不会丢失。

【问题讨论】:

redux.js.org/recipes/reducing-boilerplate 谈论性能但不回答您的问题,随着您的应用程序的增长,在返回之前检查状态的真实变化会很有趣,因为您正在使用扩展运算符更改对象引用,它总是会更改道具并重新调用渲染方法。我强烈推荐github.com/reduxjs/reselect,查看它的文档 如果这是您的问题,那么您可能根本不需要 redux。 redux 的目的是集中易于维护和可扩展的数据管理,虽然我相信你可以在这里找到很多东西 redux.js.org/recipes/reducing-boilerplate @Lelouch 那么我使用什么库?能够使用与上述方法一样少的代码行来将状态更改应用于 DOM。 请看我问题的编辑部分,谢谢! 【参考方案1】:

首先,与其在您的动作创建器中调用store.dispatch,不如返回一个对象(动作),从而简化测试和enables server rendering。

const setState = (obj) => (
  type: 'SET_USER', 
  data: obj
)

onClick=() => this.props.setState(...)

// bind the action creator to the dispatcher
connect(mapStateToProps,  setState )(User)

您还应该使用ES6 class 而不是React.createClass

回到主题,更专业的动作创建者应该是这样的:

const setSomeField = value => (
  type: 'SET_SOME_FIELD',
  value,
);
...
case 'SET_SOME_FIELD': 
  return  ...state, someField: action.value ;

这种方法相对于通用方法的优势

1。更高的可重用性

如果在多个地方设置了someField,则调用setSomeField(someValue) 比调用setState( someField: someValue ) 更干净。

2。更高的可测试性

您可以轻松地测试 setSomeField 以确保它仅正确更改相关状态。

使用通用setState,您也可以测试setState( someField: someValue ),但不能直接保证您的所有代码都会正确调用它。 例如。您团队中的某个人可能会打错字并致电 setState( someFeild: someValue )

结论

缺点并不十分明显,因此如果您认为值得为您的项目做出权衡,那么使用通用动作创建器来减少专用动作创建器的数量是完全可以的。

编辑

关于您将reducer 和actions 放在同一个文件中的建议:通常最好将它们放在modularity 的单独文件中;这是一个一般原则,并非 React 独有。

但是,您可以将相关的 reducer 和 action 文件放在同一个文件夹中,这可能会更好/更差,具体取决于您的项目要求。有关背景信息,请参阅 this 和 this。

您还需要为您的根减速器导出userReducer,除非您使用多个存储区,即generally not recommended。

【讨论】:

性能方面,这两种方法是一样的吧? Aka 这两种方法都会导致完全重新渲染【参考方案2】:

我主要使用 redux 来缓存 API 响应,这里有几个我认为它是有限的情况。

1) 如果我调用具有相同 KEY 但转到不同对象的不同 API 怎么办?

2) 如果数据是来自套接字的流,我该如何处理?我是否需要迭代对象以获取类型(因为类型将在标头中并在有效负载中响应)或要求我的后端资源使用特定模式发送它。

3) 如果我们使用无法控制我们获得的输出的第三方供应商,这对于 api 也会失败。

控制数据的去向总是很好。在非常大的应用程序中,例如网络监控应用程序如果我们有相同的 KEY 和松散类型的 JavaScript 可能会以一种很奇怪的方式结束这一点,这只适用于我们完全控制数据的少数情况,很少有像这个应用程序这样的东西。

【讨论】:

【参考方案3】:

好的,我只想写我自己的答案:

在使用 redux 时问自己两个问题:

    我是否需要跨多个组件访问数据?

    这些组件是否在不同的节点树上?我的意思是它不是子组件。

    如果您的回答是肯定的,那么请对这些数据使用 redux,因为您可以通过 connect() API 轻松地将这些数据传递给您的组件,这使得它们成为 containers

有时,如果您发现自己需要将数据传递给父组件,那么您需要重新考虑您的状态所在的位置。有一个东西叫Lifting the State Up.

如果您的数据只对您的组件重要,那么您应该只使用setState 来保持您的范围紧凑。示例:

class MyComponent extends Component 
   constructor() 
       super()
       this.state= name: 'anonymous' 
   

   render() 
       const  name  = this.state
       return (<div>
           My name is  name .
           <button onClick=()=>this.setState( name: 'John Doe' )>show name</button>
       </div>)
   

还要记住保持数据的单向数据流。如果首先数据已经可以被其父组件访问,请不要只将组件连接到 redux 存储,如下所示:
<ChildComponent yourdata=yourdata />
如果您需要从子组件更改父组件的状态,只需将函数的上下文传递给子组件的逻辑即可。示例:

在父组件中

updateName(name) 
    this.setState( name )


render() 
    return(<div><ChildComponent onChange=::this.updateName /></div>)

在子组件中

<button onClick=()=>this.props.onChange('John Doe')

这是一个很好的article about this.

只要练习一下,一旦您知道如何正确地抽象应用程序以分离关注点,一切都会开始变得有意义。在这些问题上,composition vs ihhertitance 和 thinking in react 非常适合阅读。

【讨论】:

【参考方案4】:

我开始写package 以使其更简单、更通用。也是为了提高性能。它仍处于早期阶段(38% 的覆盖率)。这里有一个小 sn-p(如果你可以使用新的 ES6 特性),但也有其他选择。

import  create_store  from 'redux';
import  create_reducer, redup  from 'redux-decorator';

class State 
    @redup("Todos", "AddTodo", [])
    addTodo(state, action) 
        return [...state,  id: 2 ];
    
    @redup("Todos", "RemoveTodo", [])
    removeTodo(state, action) 
        console.log("running remove todo");
        const copy = [...state];
        copy.splice(action.index, 1);
        return copy;
    

const store = createStore(create_reducer(new State()));

你甚至可以嵌套你的状态:

class Note
        @redup("Notes","AddNote",[])
        addNote(state,action)
            //Code to add a note
        
    
    class State
        aConstant = 1
        @redup("Todos","AddTodo",[])
        addTodo(state,action)
            //Code to add a todo
        
        note = new Note();
    
    // create store...
    //Adds a note
    store.dispatch(
        type:'AddNote'
    )
    //Log notes
    console.log(store.getState().note.Notes)

NPM 上有很多可用的文档。一如既往,随时贡献!

【讨论】:

【参考方案5】:

在设计 React/Redux 程序时要做出的一个关键决定是把业务逻辑放在哪里(它必须放在某个地方!)。

它可以放入 React 组件、动作创建器、reducer 或这些组件的组合中。通用的 action/reducer 组合是否合理取决于业务逻辑的走向。

如果 React 组件完成了大部分业务逻辑,那么 action creators 和 reducer 可以非常轻量级,并且可以按照您的建议放入单个文件中,除了使 React 组件更复杂之外,没有任何问题。

大多数 React/Redux 项目似乎有很多用于 action creators 和 reducers 的文件的原因是因为一些业务逻辑放在里面,如果使用泛型方法会导致文件非常臃肿.

就我个人而言,我更喜欢使用非常简单的 reducer 和简单的组件,并且有大量的动作来抽象出复杂性,例如从 Web 服务请求数据到动作创建者中,但“正确”的方式取决于项目手。

快速说明:如https://***.com/a/50646935 中所述,该对象应从setState 返回。这是因为在调用store.dispatch 之前可能需要进行一些异步处理。

下面是减少样板的示例。在这里,使用了通用化简器,它减少了所需的代码,但只能在其他地方处理逻辑,以便使操作尽可能简单。

import ActionType from "../actionsEnum.jsx";

const reducer = (state = 
    // Initial state ...
, action) => 
    var actionsAllowed = Object.keys(ActionType).map(key => 
        return ActionType[key];
    );
    if (actionsAllowed.includes(action.type) && action.type !== ActionType.NOP) 
        return makeNewState(state, action.state);
     else 
        return state;
    


const makeNewState = (oldState, partialState) => 
    var newState = Object.assign(, oldState);
    const values = Object.values(partialState);
    Object.keys(partialState).forEach((key, ind) => 
        newState[key] = values[ind];
    );
    return newState;
;

export default reducer;

tldr 这是在开发早期做出的设计决策,因为它会影响大部分程序的结构。

【讨论】:

【参考方案6】:

性能方面并不多。但是从设计的角度来看还是不少的。通过拥有多个 reducer,您可以分离关注点——每个模块只关注它们自己。通过让动作创建者添加一个间接层 - 允许您更轻松地进行更改。最后它仍然取决于,如果您不需要这些功能,通用解决方案有助于减少代码。

【讨论】:

【参考方案7】:

首先,一些terminology:

action:我们想要分派所有个reducer的消息。它可以是任何东西。通常它是一个简单的 javascript 对象,例如 const someAction = type: 'SOME_ACTION', payload: [1, 2, 3] 动作类型:动作创建者用来构建动作的常量,reducers 用来了解他们刚刚收到的动作。您可以使用它们来避免在动作创建器和减速器中输入'SOME_ACTION'。您可以定义类似 const SOME_ACTION = 'SOME_ACTION' 的操作类型,以便您可以在操作创建器和化简器中使用 import 它。 动作创建者:创建动作并将其分派给减速器的函数。 reducer:接收所有分派到商店的动作的函数,它负责更新该redux 商店状态 strong>(如果您的应用程序很复杂,您可能有多个商店)。

现在,问题来了。

我认为通用动作创建器不是一个好主意。

您的应用程序可能需要使用以下操作创建器:

fetchData()
fetchUser(id)
fetchCity(lat, lon)

在单个动作创建器中实现处理不同数量参数的逻辑对我来说听起来不合适。

我认为拥有许多小功能会更好,因为它们有不同的职责。例如,fetchUser 应该与 fetchCity 没有任何关系。

我首先为我的所有动作类型和动作创建者创建一个模块。如果我的应用程序增长,我可能会将动作创建者分成不同的模块(例如actions/user.jsactions/cities.js),但我认为为动作类型设置单独的模块有点矫枉过正。


对于 reducer,我认为如果您不必处理太多动作,单个 reducer 是一个可行的选择。

reducer 接收动作创建者发送的所有动作。然后,通过查看action.type,它创建了存储的新状态。由于无论如何您都必须处理所有传入的操作,我发现将所有逻辑放在一个地方很好。如果您的应用程序增长,这当然会开始变得困难(例如,处理 20 个不同操作的 switch/case 不是很容易维护)。

您可以从单个 reducer 开始,然后转移到多个 reducer,然后使用 combineReducer 函数将它们组合成一个根 reducer。

【讨论】:

以上是关于使用通用动作和减速器简化 redux的主要内容,如果未能解决你的问题,请参考以下文章

Redux 我可以在单独的减速器中使用一种动作类型吗?

Redux - reducer 与动作的关系

我的减速机应该处理@@ redux-saga-test-plan / INIT动作吗?

reducer.state.props 在嵌套动作 react/redux 中未定义

Redux 动作/reducers 与直接设置状态

所有的 redux 动作都应该有相应的 reducer 吗?