Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Which should be simpler, actions or reducers?

This is one way of writing actions when using thunk, which results in reducers that are very simple.

   getCurrentUserPicture(){
        return (dispatch,getState) => {
            dispatch({type: "isloading", isLoading: true}); // shows a loading dialog
            dispatch({type: "errorMessage"}); // clear errorMessage
            dispatch({type: "warningMessage"}); // clear warningMessage

            const userId = getState().user.get("currentUser").id;
            getUserPicture(userId) // get from backend
                .then(picture => {
                    dispatch({type: "userPicture", picture});
                    dispatch({type: "isLoading", isLoading: false}); 
                }
            )
            .catch(e=>{
                dispatch({type: "errorMessage", e});
                dispatch({type: "isLoading", isLoading: true});
                }
            )
        }
    }

With the reducer including the likes of:

export reducer(state = initialState, action = {}) {
    switch(action.type) {
        case "isLoading":
            return state.set("isLoading", action.isLoading)

Here is another approach where the actions are "cleaner" but the reducers are more complex:

   getCurrentUserPicture(){
        return (dispatch,getState) => {
            dispatch({type: "gettingCurrentUserPicture", true});

            const userId = getState().user.get("currentUser").id;
            getUserPicture(userId)
                .then(picture => {
                    dispatch({type: "retrievedCurrentUserPicture", picture}); 
                }
            )
            .catch(e=>{
                dispatch({type: "errorRetrievedCurrentUserPicture", e});
                }
            )
        }
    }

In the reducer for the above action you would have for example:

export reducer(state = initialState, action = {}) {
    switch(action.type) {
        case "gettingCurrentUserPicture":
            return state.set("isLoading", true)
                        .delete("errorMessage")
                        .delete("warningMessage")

Is one approach better than the other?

like image 788
Baz Avatar asked Dec 01 '16 09:12

Baz


1 Answers

Why not both?

Actions and Reducers should both be as simple as possible.

This is easier said than done, but by introducing other concepts and patterns such as selectors, sagas and even simple utility functions/classes, the complexity can be greatly reduced on both actions and reducers.

Actions

I know the term "broadcast" is really frowned upon in the flux world - but I find it helps me refine what should belong in my actions. The action should "broadcast" to the world what just happened - it doesn't personally care what is to follow, or how many reducers choose to respond to it - its just the messenger. ie: it does not care what the app looks like after said action.

My opinion is that business logic/rules either belong here directly, or can be channeled here via helper utility functions. See the sections below about async and utility.

It should describe what happened. Describe, describe, describe

Reducers

Reducers should form a full (ish) representation of your app with the minimal amount of data possible. Where possible, keep your state tree normalized to make sure you are holding minimal state.

My opinion is that these should be a light as possible, and at most should be just basic object manipulation - adding/removing keys or updating values.

What should the state look like based on the description I just received? (from the action)

Approach

It sounds crazy, but rubber duck debugging (in this case rubber duck programming) really helps for planning your redux structure.

I will literally speak (sometimes even outloud) through the steps like, for example:

  • You hover over the post title and click edit
  • The app swaps to the "edit post" page
  • You can edit the title field (and the field will update)
  • You can edit the body field (and the field will update)
  • The app will save every 90 seconds as a draft, a little save icon will appear in the upper right during the auto saves, but you can continue to work
  • You can also save by clicking the save button at anytime. During this time you won't be able to edit, and you will be redirected to the post index page

This can loosely be translated into actions and reducers, by looking at what is describing and what is, as a result, a change in state:

  • Click edit: Action - We are describing to the app that a post of id X has been clicked
  • Swaps to "edit post" page: Reducer - When we "hear" a "post edit" change the state of the app so its now presenting the post edit page
  • Edit title/body: Action - We describe which field and what value is entered by the user.
  • Update title/body: Reducer - State of the app is changed based on fields being entered into
  • Autosave: Action/reducer/action/reducer
    • Action: We describe to the app that an autosave has began
    • Reducer: State of app changes to show autosave in progress
    • Action: We describe autosave has completed
    • Reducer: State of app changes to hide autosave in progress
  • etc

The point I am trying to make, albeit long windily, is that its not what should be simpler, its where things (kind of) belong, but most likely you will see that your actions end up being more complex with your reducers less so.

But, my actions still aren't thin..

All of the above is easy enough to say, but how do you keep your those business-y actions clean/slim/light/simple/etc ?

At the end of the day, most business logic doesn't have a lot to do with React/Redux - so they can usually be shovelled out into their utility functions or classes. Which is great for a few reasons a) easier to test because it hasn't zero linkage to React/Redux and b) keeps your actions light, and c) more encapsulated - having minimal knowledge of react/redux is not a bad thing.

It's all just Javascript at the end of the day - there is nothing wrong with importing custom business classes or utility functions.

But but, what about async..

Async typically starts to clutter up actions really quickly. Saga's are really worth a look if you want to clean up your actions, but do introduce a certain learning curve if you're team isn't across generators and whatnot yet. In which case, thunks are still useful, and remember you can bundle up multiple async functions into a single promise that a thunk can play off (which again, that grouped promise can be separated out into a utility function class - like generateSaveSequencePromise() which might wrap up 4 or 5 async fetches, and return a single promise).

Side note - you should minimize dispatching multiple times from a single stream like in your first example. If possible, try to create a parent action that groups the entire action into one. So using your very first example, it might be:

// action
dispatch({type: "FETCHING_USER_IMAGE", id: userId });

And then your various reducers should clear their message queues, or whatnot if they "hear" that type come through.

like image 120
Chris Avatar answered Oct 19 '22 23:10

Chris