[WIP] Proposal: Refactoring our Redux

ui
core

#1

Hi all,

I would like to propose a new way to structure Redux code.
In this thread I am going to introduce two concepts: middleware and HOR (high order reducer).
I believe we can benefit a lot from these and would like to know what you think.

Middleware

When I think of Redux, I would like to imagine the following diagram:

sAmo4FHkRR0Bbqs7RvtSF9A

Why? Because it is very easy to comprehend. I know I have a view that when interacting with it an action is dispatched which ends up with updated state that is reflected on the view back. Unidirectional flow makes the UI predictable and I like this!

However, in reality it looks like this:

sAmo4FHkRR0Bbqs7RvtSF9A(1)

What is this middleware?

The middleware is the part that digests actions before they reach the reducer. It’s useful when the action is async such as API call.

Wait, you’re probably talking about Redux-Thunk, right?

Yes and no. Redux-Thunk is a middleware but I am talking about something else. I think having only Thunk in our tools belt is limiting us. Let me explain:

I was satisfied with the current way async calls were handled using Thunk. Nevertheless, it was only after wroking closely with Ron on the Autocomplete component that I felt like there must be a better way to do it.

What I mean is that when I have a complex business logic my Thunk function might get bigger and cumbersome and the logic scatters between Thunk and internal methods in the component.

For example, in the auto completion component, we had to use debounce in the actions and also in the component in order to achieve a reasonable user experience. This works well and I’m really OK with this new even though to my taste this smells. No, it doesn’t mean the code should haven’t been merged, it means we didn’t use the right tools. Using only Thunk was great when we had to do a simple async call but when we want to add more to it such as debounce or suspense it would get uglier.

Enter middlewares

Do you remember the second diagram? Middleware was that thing between the UI and the Reducer. This is where we can put all our side effects or common business logic and make the side effect transparent to front end developers so they can focus on the UI/Redux flow.

In the auto complete example, instead of wrapping specific methods with debounce we can have a middleware that will debounce an action. The action will contain a meta attribute which specifies the duration of the debounce. We can take it further and turn our ajaxRequestAction into a middleware and now instead of dispatching a redux-thunk we will dispatch a simple object which looks like this:

{
  type: "API",
  meta: {
    url: "/models/autocomplete",
    states: ["MODELS_API_REQUEST", "MODELS_API_SUCCESS", "MODELS_API_FAILURE"],
    items: {
      controller: "models",
    },
    debounce: 300,
  }
}

The debounce middleware checks if an actions has meta.debounce, if it does it will update the action till 300 ms will pass and then will pass it to the next middleware which is the API fetcher.

Then the API fetcher middleware will check that action.type is API and from the meta it will take the URL and do an ajax call not before dispatching another action of type “MODELS_API_REQUEST”. If the ajax call was success then a new action with the response payload and type “MODELS_API_SUCCESS” will be dispatched. Otherwise, “MODELS_API_FAILURE” typed action with the error as the payload will be dispatched.

I can even take it to the next step and pass to the API a function that normalizes the response but that can’t wait.

I like it more than Thunk because:

  1. Testing: instead of mocking the ajaxRequestAction and checking the arguments passed to it, I can simply take a snapshot of the object returned from the action in my tests.
  2. As a front end developer I don’t need to think about a function that dispatches other actions. I am staying in a more simple realm of actions creators returning JSON objects rather than returning functions that return JSON objects.

HOR

High order reducers similarly to HOC (high order component) was meant to wrap a reducer with a new reducer and to make the code more D.R.Y.

For example, it’s very common to see components that toggles a modal or a menu. This means that the same logic repeats itself in many places. This might be code-smell since I am testing same logic in many places and also if I change the logic in one place I will probably have to change it in other places.

However, there is a solution for that. I can write a reducer that takes a reducer and return a new enhanced reducer:

const enhanceReducer = (reducer) => (state, action) => {
  newState = reducer(action);
  if (action.type.includes("TOGGLE")) {
    return {
      ...newState, 
        show: !newState.show
      }
    }
  }
}

In this reducer I am taking a component reducer and returning a new reducer. The new reducer gets a state and action and pass it to the wrapped reducer and then it checks that action.type has “TOGGLE”. If it does a new state is created: changing the show from flase/true to true/false.

We can similarly apply this to reducing API calls, which IMHO repeats itself very often.


As I mentioned above this is my opinion and I would like to share this with you to get some feedback.
I am happy with the current configuration/state/structure but I believe we can always evolve and improve. So please feel free to tell me what you think but be nice :smile:.

PS: this is still work in progress and I’m probably going to update it a lot in the next hours.

Thanks.


Foreman UI newsletter
#2

Interesting thoughts!

I’m not sure if its just me, but I can’t see the diagrams mentioned.

My initial question is - is this something you want to change in new code going forward or are you talking about doing a refactoring of existing redux code? Apologies if I missed this.


#3

Hey @John_Mitsch,

I’m not sure if its just me, but I can’t see the diagrams mentioned.

I uploaded the diagrams again, can you see them now?

My initial question is - is this something you want to change in new code going forward or are you talking about doing a refactoring of existing redux code?

I would like to refactor existing redux code. However, I lean more towards using high order reducers in the code. I believe it’s easy to test and to implement than migrating from Thunk to chain of middlewares. Moreover, from what I have seen many duplicate code is around that area while using middleware only helps one component.


#4

yup! Thanks!

I have to read more into this before I make any coherent comment, but maybe those more experienced with React/Redux will jump in with their thoughts


#5

Adding middlewares for such common patterns make a lot of sense to me. It can save a lot of boilerplate, make devs’ lives easier and help with standardizing format of our actions. In addition to that we could also make the api action creators simpler.
If we’re going to start this effort (and I support that) we need to think about the actions’ structure well and document it.

I also agree with extracting the common patterns from reducers. I’m not 100% convinced that HOR are the best way to go for us. It’s definitely very clean design that allows simple composition. My slight concern is it might be difficult to discover for new developers where “toggling” (in this example) takes place and might feel like a magic. Maybe a set of helper functions that can be used in reducers could serve the same purpose but result in better readability.

BTW kudos for such detailed documentation of the proposal @boaz1337.


#6

@boaz1337 Got a chance to look at this more, I agree with @Tomas_Strachota that adding middleware for common patterns we have in our workflow to DRY things up is good, I do like the intention of the post. I’m also not convinced about using a HOR and more abstraction, but maybe I have to try it out in an example before saying anything.

Can you explain more why debounce was needed in the first place and why we would need to have something like this as a permanent option in our redux workflow?


#7

Thank you for this proposal @boaz1337

I believe the middlewares are a great idea when it comes to API operations and would love to see it implemented and used, I think it can bring much more simplicity to our actions and tests.

As for debounce, I’m not convinced the best way is middleware, won’t it be simpler to wrap the action itself with a debounce helper?

HOR is a new concept for me and I need to see some more practical examples to have an opinion about it, but I agree we must find a solution for reusing reducers and if HOR is the best way I agree we should embrace it.


#8

Thank you all.

I am glad there is some attraction around this. I’m going to elaborate more in the up coming days and give concrete examples.

Boaz.


#9

Thanks for the detailed write up @boaz1337. Looking forward to seeing some examples.