使用通用操作简化Redux & Reducer

bybem2ql  于 2023-06-06  发布在  其他
关注(0)|答案(8)|浏览(164)

在React-Redux项目中,人们通常会为每个连接的组件创建多个action & reducer。但是,这会为简单的数据更新创建大量代码。
使用单个通用操作& 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.所有操作的集中文件
1.所有reducer的集中文件
问题是,为什么是两步流程?下面是另一个架构建议:
创建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个文件封装了User概念所需的所有内容。* 为什么这是个坏习惯 * IMO,它使程序员的生活更容易,其他程序员 * 可以从上到下阅读文件以理解业务逻辑 *,他们不需要在action/reducer文件之间来回切换。见鬼,甚至redux-thunk都不需要!你甚至可以一个接一个地测试这些功能。所以可测试性不会丢失。

vvppvyoh

vvppvyoh1#

首先,它应该返回一个对象(action),而不是在action creator中调用store.dispatch,这简化了测试并启用了服务器渲染。

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

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

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

你也应该使用ES6类而不是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 })}

总结

缺点并不是很明显,所以如果你相信这对你的项目是值得的,那么使用通用的动作创建器来减少专门的动作创建器的数量是非常好的。

编辑

关于你建议把reducers和actions放在同一个文件中:一般来说,对于modularity,最好将它们保存在单独的文件中;这是一个通用原则,并不是React独有的。
但是,您可以将相关的reducer和action文件放在同一个文件夹中,这可能会更好/更差,具体取决于您的项目需求。参见thisthis了解一些背景知识。
您还需要为您的root reducer导出userReducer,除非您使用多个商店,这通常不建议使用。

vyswwuz2

vyswwuz22#

我主要使用redux来缓存API响应,这里有几个我认为它是有限的情况。
1)如果我正在调用不同的API,它们具有相同的KEY,但指向不同的Object,该怎么办?
2)如果数据是来自套接字的流,我该如何处理?我是否需要迭代对象以获取类型(因为类型将在头部中,而响应将在有效负载中),或者要求我的后端资源使用特定的模式发送它。
3)如果我们使用第三方供应商,我们无法控制我们得到的输出,这也会失败。

控制数据的去向总是很好的。在应用程序中,这是非常大的东西,如网络监控应用程序我们可能会最终覆盖数据,如果我们有相同的键和JavaScript被松散键入可能会结束这一点很多奇怪的方式这只适用于少数情况下,我们有完全控制的数据,这是非常少的一些事情,如这个应用程序。

puruo6ea

puruo6ea3#

好吧我就自己写答案:
1.在使用Redux时,请问自己以下两个问题:
1.我是否需要跨多个组件访问数据?
1.这些组件是否位于不同的节点树上?我的意思是它不是子组件。
如果你的答案是肯定的,那么使用redux来处理这些数据,因为你可以通过connect() API轻松地将这些数据传递给你的组件,这使得它们成为containers
1.有时,如果您发现自己需要将数据传递给父组件,那么您需要重新考虑状态的位置。有一个东西叫Lifting the State Up.
1.如果您的数据只对您的组件重要,那么您应该只使用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 store,就像这样:
<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 ihhertitancethinking in react是一本非常好的读物。
33qvvth1

33qvvth14#

我开始写一个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上有很多可用的文档。像往常一样,随时准备贡献!

ykejflvf

ykejflvf5#

在设计React/Redux程序时要做的一个关键决定是将业务逻辑放在哪里(它必须放在某个地方!).
它可以在React组件中,在动作创建器中,在还原器中,或者这些的组合中。泛型操作/reducer组合是否合理取决于业务逻辑的走向。
如果React组件完成大部分业务逻辑,那么动作创建器和还原器可以非常轻量级,并且可以按照您的建议放入单个文件中,除了使React组件更加复杂之外,没有任何问题。
大多数React/Redux项目似乎有很多文件用于action creators和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这是在开发早期做出的设计决策,因为它会影响程序的大部分结构。

ztigrdn8

ztigrdn86#

智慧的表现不多。从设计的Angular 来看,有不少。通过使用多个reducer,您可以实现关注点的分离-每个模块只关注它们自己。通过操作创建器,您可以添加一个间接层-允许您更轻松地进行更改。最后,它仍然取决于,如果你不需要这些功能,一个通用的解决方案有助于减少代码。

lyr7nygr

lyr7nygr7#

首先,一些terminology

*action:我们希望派发所有reducer的消息。它可以是任何东西通常它是一个简单的Javascript对象,如const someAction = {type: 'SOME_ACTION', payload: [1, 2, 3]}
*动作类型:一个常数,由动作创建者用来构建动作,并由还原器用来理解他们刚刚接收到的动作。使用它们可以避免在动作创建器和缩减器中输入'SOME_ACTION'。您定义了一个类似const SOME_ACTION = 'SOME_ACTION'的操作类型,以便可以在操作创建器和还原器中对它进行import
*动作创建者:创建动作并将其分派给reducer的函数。
*减速器:一个函数,它接收所有被分派到store的动作,并且它负责更新该reduxstore状态(如果你的应用很复杂,你可能有多个store)。

现在,回到问题上来。
我认为一个通用的动作创作者不是一个好主意。
您的应用程序可能需要使用以下操作创建器:

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

在单个操作创建器中实现处理不同数量参数的逻辑对我来说听起来不太正确。
我认为有许多小功能会更好,因为它们有不同的职责。例如,fetchUser不应该与fetchCity有任何关系。
我首先为我所有的动作类型和动作创建者创建一个模块。如果我的应用程序增长,我可能会将动作创建器分离到不同的模块中(例如actions/user.jsactions/cities.js),但我认为为动作类型设置单独的模块有点大材小用。
至于reducer,我认为如果你不需要处理太多的动作的话,一个reducer是一个可行的选择。
reducer接收由动作创建者分派的所有动作。然后,通过查看action.type,它创建了存储的新状态。由于您无论如何都必须处理所有传入的操作,因此我发现将所有逻辑放在一个地方是很好的。当然,如果您的应用程序增长,这将变得困难(例如一个switch/case来处理20个不同的动作是不太容易维护的)。
您可以从一个reducer开始,然后移动到多个reducer,并使用combineReducer函数将它们组合在根reducer中。

axr492tv

axr492tv8#

任何一个使用redux工具包的人,他想像这样更新状态

/*
    const InventoryGlobalState = {
      filter: {
        crieteria: {
          newItem: {} as any,
          added: [] as any
        },
      },
    }
*/

const {inventoryState, inventoryActions} = inventoryStore()

useEffect(() => {
  inventoryActions.filter.crieteria.newItem.set(state);
}, [state])

然后他可以看到按照此文档https://mohsin-ejaz.gitbook.io/redux/redux-easy

相关问题