通过通用动作简化redux&减速器

时间:2018-05-27 10:57:04

标签: javascript reactjs redux react-redux jsx

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

使用单个通用操作& 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. 包含所有操作的集中文件
  2. 包含所有Reducer的集中文件
  3. 问题是,为什么需要两个步骤?这是另一个建筑建议:

    创建 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
    

    请仔细阅读上面代码部分的评论。

    现在没有一堆行动/调度员/减速器。您有 1个文件封装了用户概念所需的所有内容为什么这是一个不好的做法? IMO,它使程序员的生活更容易,其他程序员可以从上到下阅读文件来理解业务逻辑,他们不会&#39 ; t需要在action / reducer文件之间来回切换。哎呀,即使redux-thunk也不需要!您甚至可以逐个测试功能。因此可测试性不会丢失。

7 个答案:

答案 0 :(得分:5)

首先,它不是在您的动作创建者中调用$ mkdir ~/src/build-myproject $ cd ~/src/build-myproject $ /opt/local/libexec/qt5/bin/qmake ../myproject $ make ,而是应该返回一个对象(动作),这样可以简化测试和enables server rendering

store.dispatch

您还应该使用ES6 class代替const setState = (obj) => ({ type: 'SET_USER', data: obj }) onClick={() => this.props.setState(...)} // bind the action creator to the dispatcher connect(mapStateToProps, { setState })(User)

回到主题,一个更专业的动作创建者将是:

React.createClass

这种方法优于您的通用方法

1。更高的可重用性

如果在多个地方设置const setSomeField = value => ({ type: 'SET_SOME_FIELD', value, }); ... case 'SET_SOME_FIELD': return { ...state, someField: action.value }; ,则调用someFieldsetSomeField(someValue)更清晰。

2。更高的可测性

您可以轻松地测试setState({ someField: someValue })}以确保它只是正确地改变了相关状态。

使用通用setSomeField,您也可以测试setState,但没有直接保证您的所有代码都能正确调用它。
例如。您团队中的某个人可能会输入拼写错误并致电setState({ someField: someValue })}

结论

缺点并不是很重要,因此如果您认为值得为您的项目进行权衡,那么使用通用动作创建器来减少专门的动作创建者的数量是完全可以的。

修改

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

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

除非您使用多个generally not recommended商店,否则您还需要为根减速器导出setState({ someFeild: someValue })}

答案 1 :(得分:1)

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

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

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

3)如果我们使用的是某些第三方供应商而我们无法控制产量,这也无法实现api。

控制哪些数据到哪里总是好的。对于像网络监控应用程序这样非常大的应用程序如果我们可能最终覆盖数据有相同的KEY和JavaScript被释放键入可能会以很多奇怪的方式结束这只适用于我们完全控制数据的少数情况,这很少像这个应用程序。

答案 2 :(得分:1)

我开始编写package以使其更简单,更通用。也是为了提高性能。它还处于早期阶段(覆盖率为38%)。这里有一个小片段(如果您可以使用新的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上提供了大量文档。一如既往,随时做出贡献!

答案 3 :(得分:0)

表现明智不多。但从设计角度来看,相当多。通过使用多个减速器,您可以分离关注点 - 每个模块只关注自己。通过让动作创建者添加一层间接 - 让您更轻松地进行更改。最后,它仍然取决于,如果您不需要这些功能,通用解决方案有助于减少代码。

答案 4 :(得分:0)

首先,一些terminology

  • 操作:我们希望发送所有缩减器的消息。它可以是任何东西。通常它是一个简单的Javascript对象,如const someAction = {type: 'SOME_ACTION', payload: [1, 2, 3]}
  • 操作类型:操作创建者用来构建操作的常量,以及reducers用于了解他们刚接收到的操作的常量。您可以使用它们来避免在动作创建者和减少者中键入'SOME_ACTION'。您可以定义一个操作类型,例如const SOME_ACTION = 'SOME_ACTION',以便您可以在动作创建者和减少者中import
  • 动作创建者:创建动作并将其发送给减速器的函数。
  • reducer :一个接收调度到商店的所有操作的函数,它负责更新该redux 状态商店(如果您的应用程序很复杂,您可能会有多个商​​店。)

现在,问题。

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

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

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

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

我认为拥有许多小功能要好得多,因为他们有不同的职责。例如,fetchUser不应与fetchCity有任何关系。

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

至于减速器,我认为如果你不需要处理太多的动作,单个减速器是一个可行的选择。

reducer接收动作创建者分派的所有动作。然后,通过查看action.type,它会创建商店的新状态。既然你必须处理所有传入的动作,我发现将所有逻辑放在一个地方很好。如果您的应用程序增长,这当然会变得很困难(例如,处理20个不同操作的switch/case不可维护)。

你可以从一个减速器开始,转移到几个减速器,然后将它们组合在一个带有combineReducer功能的根减速器中。

答案 5 :(得分:0)

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

  • 使用redux时请问自己这两个问题:

    1. 我是否需要跨多个组件访问数据?
    2. 这些组件是否在不同的节点树上?我的意思是它不是儿童成分。

      如果你的答案是肯定的,那么就可以使用redux来获取这些数据,因为你可以通过connect() API轻松地将这些数据传递给你的组件,这些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.

答案 6 :(得分:0)

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

它可以包含在React组件,动作创建器,减速器或其组合中。通用动作/减速器组合是否合理取决于业务逻辑的位置。

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

大多数React / Redux项目似乎有很多文件用于操作创建者和reducers的原因是因为某些业务逻辑被放在那里,因此如果使用泛型方法会导致文件非常臃肿

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

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

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

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 这是一个在开发早期制定的设计决策,因为它会影响程序的大部分结构。