17

In React-Redux project, people usually create multiple actions & reducers for each connected component. However, this creates a lot of code for simple data updates.

Is it a good practice to use a single generic action & reducer to encapsulate all data changes, in order to simplify and fasten app development.

What would be the disadvantages or performance loss using this method. Because I see no significant tradeoff, and it makes development much easier, and we can put all of them in a single file! Example of such architecture:

// 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);

Edit

So the typical Redux architecture suggests creating:

  1. Centralized files with all the actions
  2. Centralized files with all the reducers

Question is, why a 2-step process? Here's another architectural suggestion:

Create 1 set of files containing all the setXField() that handle all the data changes. And other components simply use them to trigger changes. Easy. Example:

/** 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

Please read through the comments in the code section above.

Now instead of having a bunch of actions/dispatchers/reducers. You have 1 file encapsulating everything needed for the User concept. Why is it a bad practice? IMO, it makes programmer's life much easier, and other programmers can just read through the file from top to bottom to understand the business logic, they don't need to switch back and forth between action/reducer files. Heck, even redux-thunk isn't needed! And you can even test the functions one by one as well. So testability is not lost.

Maria
  • 3,455
  • 7
  • 34
  • 47
  • https://redux.js.org/recipes/reducing-boilerplate – Tomasz Mularczyk May 27 '18 at 11:00
  • Talking about performance but not answering your question, as your application grows it would be interesting checking real changes on state before returning, as you're changing object reference using the spread operator, it will always change props and call render methods all over again. I strongly recommend https://github.com/reduxjs/reselect, check out its docs – Canta Jun 02 '18 at 13:22
  • If that is your problem then you many NOT need redux at all. The purpose for redux is centralized easily maintainable and scalable data management that requires more boilerplate in exchange for structure although i'm sure you can find alot of stuff here https://redux.js.org/recipes/reducing-boilerplate – Lelouch Jun 05 '18 at 17:04
  • @Lelouch So what library do I use? To be able to apply state change to DOM using as few lines of code as the method above. – Maria Jun 06 '18 at 01:50
  • Please see the edit section of my question, thanks! – Maria Jun 06 '18 at 03:03
  • Take a look at [ReduxSauce](https://github.com/infinitered/reduxsauce) – Medet Tleukabiluly Jun 07 '18 at 20:10

8 Answers8

5

Firstly, instead of calling store.dispatch in your action creator, it should return an object (action) instead, which simplifies testing and 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)

You should also use ES6 class instead of React.createClass.

Back to the topic, a more specialised action creator would be something like:

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

Advantages of this approach over your generic one

1. Higher reusability

If someField is set in multiple places, it's cleaner to call setSomeField(someValue) than setState({ someField: someValue })}.

2. Higher testability

You can easily test setSomeField to make sure it's correctly altering only the related state.

With the generic setState, you could test for setState({ someField: someValue })} too, but there's no direct guarantee that all your code will call it correctly.
Eg. someone in your team might make a typo and call setState({ someFeild: someValue })} instead.

Conclusion

The disadvantages are not exactly significant, so it's perfectly fine to use the generic action creator to reduce the number of specialised action creators if you believe it's worth the trade-off for your project.

EDIT

Regarding your suggestion to put reducers and actions in the same file: generally it's preferred to keep them in separate files for modularity; this is a general principle that is not unique to React.

You can however put related reducer and action files in the same folder, which might be better/worse depending on your project requirements. See this and this for some background.

You would also need to export userReducer for your root reducer, unless you are using multiple stores which is generally not recommended.

Roy Wang
  • 11,112
  • 2
  • 21
  • 42
  • performance-wise, both methods are the same right? Aka both methods will cause a full re-render – Maria Jun 02 '18 at 12:36
1

I mostly use redux to cache API responses mostly, here are few cases where i thought it is limited.

1) What if i'm calling different API's which has the same KEY but goes to a different Object?

2) How can I take care if the data is a stream from a socket ? Do i need to iterate the object to get the type(as the type will be in the header and response in the payload) or ask my backend resource to send it with a certain schema.

3) This also fails for api's if we are using some third party vendor where we have no control of the output we get.

It's always good to have control on what data going where.In apps which are very big something like a network monitoring application we might end up overwriting the data if we have same KEY and JavaScript being loosed typed may end this to a lot weird way this only works for few cases where we have complete control on the data which is very few some thing like this application.

karthik
  • 1,100
  • 9
  • 21
1

Okay i'm just gonna write my own answer:

  • when using redux ask yourself these two questions:

    1. Do I need access to the data across multiple components?
    2. Are those components on a different node tree? What I mean is it isn't a child component.

      If your answer is yes then use redux for these data as you can easily pass those data to your components via connect() API which in term makes them containers.

  • At times if you find yourself the need to pass data to a parent component, then you need to reconsider where your state lives. There is a thing called Lifting the State Up.

  • If your data only matters to your component, then you should only use setState to keep your scope tight. Example:

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>)
   }
}
  • Also remember to maintain unidirectional data flow of data. Don't just connect a component to redux store if in the first place the data is already accessible by its parent component like this:
<ChildComponent yourdata={yourdata} />
  • If you need to change a parent's state from a child just pass the context of a function to the logic of your child component. Example:

In parent component

updateName(name) {
    this.setState({ name })
}

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

In child component

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

Here is a good article about this.

Lelouch
  • 910
  • 6
  • 19
1

I started writing a package to make it easier and more generic. Also to improve performance. It's still in its early stages (38% coverage). Here's a little snippet (if you can use new ES6 features) however there is also alternatives.

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()));

You can also even nest your 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)

Lots of documentation available on NPM. As always, feel free to contribute!

Eladian
  • 958
  • 10
  • 29
1

A key decision to be made when designing React/Redux programs is where to put business logic (it has to go somewhere!).

It could go in the React components, in the action creators, in the reducers, or a combination of those. Whether the generic action/reducer combination is sensible depends on where the business logic goes.

If the React components do the majority of the business logic, then the action creators and reducers can be very lightweight, and could be put into a single file as you suggest, without any problems, except making the React components more complex.

The reason that most React/Redux projects seem to have a lot of files for action creators and reducers because some of the business logic is put in there, and so would result in a very bloated file, if the generic method was used.

Personally, I prefer to have very simple reducers and simple components, and have a large number of actions to abstract away complexity like requesting data from a web service into the action creators, but the "right" way depends on the project at hand.

A quick note: As mentioned in https://stackoverflow.com/a/50646935, the object should be returned from setState. This is because some asynchronous processing may need to happen before store.dispatch is called.

An example of reducing boilerplate is below. Here, a generic reducer is used, which reduces code needed, but is only possible the logic is handled elsewhere so that actions are made as simple as possible.

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 It is a design decision to be made early on in development because it affects how a large portion of the program is structured.

A Jar of Clay
  • 5,622
  • 6
  • 25
  • 39
0

Performance wise not much. But from a design perspective quite a few. By having multiple reducers you can have separation of concerns - each module only concerned with themselves. By having action creators you add a layer of indirection -allowing you to make changes more easily. In the end it still depends, if you don't need these features a generic solution helps reduce code.

0

First of all, some terminology:

  • action: a message that we want to dispatch to all reducers. It can be anything. Usually it's a simple Javascript object like const someAction = {type: 'SOME_ACTION', payload: [1, 2, 3]}
  • action type: a constant used by the action creators to build an action, and by the reducers to understand which action they have just received. You use them to avoid typing 'SOME_ACTION' both in the action creators and in the reducers. You define an action type like const SOME_ACTION = 'SOME_ACTION' so you can import it in the action creators and in the reducers.
  • action creator: a function that creates an action and dispatches it to the reducers.
  • reducer: a function that receives all actions dispatched to the store, and it's responsible for updating the state for that redux store (you might have multiple stores if your application is complex).

Now, to the question.

I think that a generic action creator is not a great idea.

Your application might need to use the following action creators:

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

Implementing the logic of dealing with a different number of arguments in a single action creator doesn't sound right to me.

I think it's much better to have many small functions because they have different responsibilities. For instance, fetchUser should not have anything to do with fetchCity.

I start out by creating a module for all of my action types and action creators. If my application grows, I might separate the action creators into different modules (e.g. actions/user.js, actions/cities.js), but I think that having separate module/s for action types is a bit overkill.


As for the reducers, I think that a single reducer is a viable option if you don't have to deal with too many actions.

A reducer receives all the actions dispatched by the action creators. Then, by looking at the action.type, it creates a new state of the store. Since you have to deal with all the incoming actions anyway, I find it nice to have all the logic in one place. This of course starts to be difficult if your application grows (e.g. a switch/case to handle 20 different actions is not very maintainable).

You can start with a single reducer, the move to several reducers and combine them in a root reducer with the combineReducer function.

jackdbd
  • 4,583
  • 3
  • 26
  • 36
0

Any One using redux toolkit and he wants to update the state like this

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

const {inventoryState, inventoryActions} = inventoryStore()

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

then he can see follow this documentation https://mohsin-ejaz.gitbook.io/redux/redux-easy

Mohsin Ejaz
  • 316
  • 3
  • 7