Idiomatic Redux: The Tao of Redux, Part 2 - Practice and Philosophy

This is a post in the Idiomatic Redux series.


More thoughts on what Redux requires, how Redux is intended to be used, and what is possible with Redux

Intro

I've spent a lot of time discussing Redux usage patterns online, whether it be helping answer questions from learners in the Reactiflux channels, debating possible changes to the Redux library APIs on Github, or discussing various aspects of Redux in comment threads on Reddit and HN. Over time, I've developed my own opinions about what constitutes good, idiomatic Redux code, and I'd like to share some of those thoughts. Despite my status as a Redux maintainer, these are just opinions, but I'd like to think they're pretty good approaches to follow :)

This is a two-part series, intended to explain why specific Redux usage patterns and practices exist, the philosophy and intent behind Redux, and what I consider to be "idiomatic" and "non-idiomatic" Redux usage.

In Part 1 - Implementation and Intent, we looked at the actual implementation of Redux, what specific limitations and constraints it requires, and why those limitations exist. From there, we reviewed the original intent and design goals for Redux, based on the discussions and statements from the authors (especially during the early development process).

In Part 2 - Practice and Philosophy, we'll investigate the common practices that are widely used in Redux apps, and describe why those practices exist in the first place . Finally, we'll examine a number of "alternative" approaches for using Redux, and discuss why many of them are possible but not necessarily "idiomatic".

Redux In Practice

There's a long list of common approaches and "best practices" that are used in Redux apps. Many of the frequent complaints about "boilerplate" involve these concepts. Let's walk through these practices and review the reasons why they're common and usually encouraged.

Actions, Action Constants, and Action Creators

These three concepts are probably the ones most commonly referred to in complaints about "boilerplate". I was going to cover each of these separately and in detail, but then I realized that there's already a well-written document that addresses why these concepts exist and why they're a good thing: the "Reducing Boilerplate" page in the Redux docs.

I'll quickly summarize the main points:

  • Actions: Describing updates as plain serializable action objects enables time-travel debugging and hot reloading
  • Action constants: Help with consistent naming conventions, make it easier to see what action types are used in an application, help prevent typos, and allow static analysis of code in IDEs
  • Action creators: Encapsulate logic around creating actions, reduce duplication, allow moving logic out of components, and act as an API.

As mentioned in Part 1, many of Redux's conventions are inherited from the original Flux Architecture concepts, and the Flux Actions and the Dispatcher section of the Flux docs does describe these ideas. My earlier post in this series, Idiomatic Redux: Why Use Action Creators?, also gives further thoughts on why use of action creators is a good practice.

There was also an interesting Twitter thread last year, where Dan responded to an article about "no action creators needed":

I wonder if it was better to not include action creators in Redux docs. People would come up with them anyway but wouldn’t blame Redux.
Separation between actions and reducers is the whole reason Redux exists. Read You Might Not Need Redux
... we failed to explain these things well enough that people see “dispatching” or “action creators” as scary
I don’t mean to blame you, the blame is on us. It means we failed to document and explain the library and concepts.
“Dispatching” is the only feature of Redux. That people try to hide from it means we didn’t explain why Redux exists well.
As also evidenced by hundreds of libraries that conflate reducers and action creators together as if actions were “local”.
Underlying all of it is, in my opinion, a basic misunderstanding of when to use Redux, and it’s our fault.

Plain object actions are a core design decision for Redux. Use of action constants and action creators is up to you as a developer, but both of those are derived from good software engineering principles like encapsulation and the ever-popular mantra of "Don't Repeat Yourself".

Defining Actions, Action Creators, and Reducers in Separate Files

As mentioned in the Redux FAQ on file/folder structure, Redux itself doesn't care at all how you organize your files. That's entirely up to you, and you should feel free to do whatever works best for your project.

Defining different types of code in different files is a natural pattern for a developer to follow. At a minimum, you'd probably want to write your Redux-related code in separate files from your React components. Reducers are their own thing, so it's reasonable to put them in one file. If you've chosen to use action creators in your project, those are a different kind of thing, so they would go in a second file. From there, you would need to make use of the same action type strings in both the action creators file and the reducers file, so it makes sense to extract them into a separate file that can be imported in both places. So, this approach isn't a requirement in any way, but it's a very natural and reasonable approach to follow.

The community-defined "ducks" pattern suggests putting action creators, constants, and reducers all together in one file, usually representing some kind of domain concept and logic. Again, Redux itself doesn't care if you do that, and it does have the benefit of minimizing the number of files that have to be touched if you update a feature.

To me, there are a couple conceptual downsides to the "ducks" pattern. One is that it guides you away from the idea of multiple slice reducers independently responding to the same action. Nothing about "ducks" prevents you from having multiple reducers respond, but having everything in one file somewhat suggests that it's all self-contained and not as likely to interact with other parts of the system. There's also some aspects of dependency chains and imports involved - if any other part of the system wants to import action creators from a duck, it will kind of drag along the reducer logic in the process. The reducers may not actually get imported elsewhere, but there's a dependency on that one file. If that doesn't bother you, feel free to use "ducks".

Personally, I lean towards a "feature folder"-type approach, where one folder contains the code for a given feature, but with separate files for each type of code. I previously wrote some of my thoughts in Practical Redux, Part 4: UI Layout and Project Structure.

Slice Reducers and Reducer Composition

This is based on the original Flux idea of having multiple "Stores" for different types of data. Also, in Flux, each Store registered itself with the Dispatcher, and each time an action was dispatched, the Dispatcher would loop through each Store to give them a chance to respond to the action.

Given the desire for Redux to store a variety of data in one tree, the straightforward approach is to have a top-level key or "slice" for each different category of data in the state tree. From there, the concern is how to initialize that state, and how to organize the logic for updating the state.

As discussed in the Structuring Reducers - Basic Reducer Structure section I wrote for the Redux docs, it's important to understand that your entire application really only has one single reducer function: the function that you've passed into createStore as the first argument. That root reducer is the only one that is required to have the (state, action) => newState signature. It's entirely possible to have that one function be directly responsible for initializing all the slices of the state tree and keeping them updated, but this once again leads us to basic software engineering principles: we split up large sections of code into smaller sections for better maintainability.

There's a natural mapping from having a Store class that manages some data and is notified when an action is dispatched, to having a plain function that is responsible for handling some data and is called when an action is dispatched. Since Flux Stores were turned into slices of state, it follows that the equivalent Redux function would be responsible for managing that slice of state. This results in a nicely recursive structure. Each slice reducer can have the same (state, action) => newState signature as the root reducer, and as far as a slice reducer is concerned, its state parameter is the entire state - it doesn't need to know that it might be a small part of a bigger tree, and could in theory be reused in another context. It's even possible, if perhaps unlikely, that a reducer used as a slice reducer in one application could be used as the root reducer in another application.

This pattern is a direct result of the transformation from Flux to Redux, and is absolutely the encouraged and idiomatic approach.

Switch Statements

Ah, the dreaded switch statement. For some reason, this is one of the most disliked aspects of typical Redux code, and I have yet to figure out why.

Reducers need to examine actions and use some kind of conditional logic to determine whether it's something they care about. You could use if/else statements, but those get repetitive pretty quickly, especially when you're only looking at a single field. A switch statement is simply an if/else that's focused on possible values for a single field, so it's the most straightforward approach for looking at the contents of action.type.

Switches and if statements are semantically equivalent, and so are lookup tables keyed by action constants. The Reducing Boilerplate docs page explicitly demonstrates how to write a function that accepts a lookup table of reducers to handle various cases, and this concept is also discussed in Structuring Reducers - Refactoring Reducers Example. (There are a couple structures that lookup tables don't handle well, such as running reducer logic in a switch's default case, or keying behavior off something other than the action.type field. Dan gave some examples in redux#1024.)

Despite that, somehow the Redux community seems to think that this is a wheel that needs to be re-invented, again and again and again. To expand on a tweet that I posted last year:

There's a classic SF humor story about a bulletin board of time travelers, and the first thing any new poster in the forum does is travel back to kill Hitler. The Redux equivalent: every new user writes a "build a reducer lookup table" function.

Switch statements are fine. If/else statements are fine. Lookup tables are fine. Just pick one and keep going :)

(As an interesting side note: a few months after Redux came out, someone wrote an article discussing how React+Redux is kind of like the classic Win32 WndProc function, including the use of switch statements. Interesting analogy, good article, and there was actually some good discussion in the HN comments.)

Middleware for Async Logic

This is another topic that's been covered in detail in existing documentation and posts. In particular, the Redux FAQ question on async logic and Dan Abramov's two Stack Overflow answers on why middleware are used for async behavior and how to handle async logic like timeouts explain things very well.

Summarizing the ideas: you can put your async logic directly into your components. However, for reusability, you probably want to extract that logic out of the components and into separate functions. Connected components potentially have access to dispatch, but can only access the state that has been extracted via mapStateToProps. Separate functions could only access dispatch or getState if the store is directly imported and referenced. That means that async logic that has been extracted into functions needs some way to interact with the store.

As discussed in Part 1, some Flux libs like Flummox had various forms of async handling built-in, but you were limited to whatever was included. For Redux, middleware was explicitly intended as a user-configurable way of adding any kind of async behavior on top of Redux.

Because middleware form a pipeline around dispatch and can modify/intercept/interact with anything coming through that pipeline, and are given references to the store's dispatch and getState methods, they form a loophole where async behavior can occur but still interact with the store during the process.

Thunks, Sagas, and Observables, Oh My!

There are dozens of existing libraries for managing side effects in Redux. That's because there's a lot of ways to write and manage asynchronous logic in Javascript, and everyone has different preferences and ideas on how to do so. As stated previously, you don't need middleware to use async logic in a Redux app, but it's the recommended and idiomatic approach.

From there, it's a question of what use cases you have, and what your preferences are for writing async logic. Side effects approaches can generally be grouped into five-ish categories. I'll link to the relevant sections of my Redux addons catalog, and point out the most popular libraries for each category:

  • Functions : use of plain functions as a general tool for whatever async logic you want to run. Popular choice: redux-thunk, which simply allows passing functions to dispatch that are then called and given dispatch and getState as arguments. Thunks are seen as the "minimum viable approach" for side effects with Redux, and are most useful for complex synchronous logic and simple async behavior (like fire-and-forget AJAX calls).
  • Promises: use of promises as arguments to dispatch, usually for dispatching actions as the promise resolves or rejects. Popular choices: redux-promise, a Redux middleware version of Andrew Clark's original promise behavior from Flummox, and redux-pack, a recent library from Leland Richardson that tries to add more convention and guidance to async transactions.
  • Generators: use of ES6 generator functions to control async flow. Popular choice: redux-saga, a powerful Redux-oriented flow control library that enables complex async workflows via background-thread-like "saga" functions.
  • Observables: use of observable / Functional Reactive Programming libraries like RxJS to create pipelines of async logic. Popular choices: redux-observable and redux-logic, which are both based on RxJS, but provide different APIs for interacting with RxJS and Redux.
  • Other: assorted other approaches for async behavior. Popular choice: redux-loop, which allows reducers to return descriptions of side effects, similar to how the Elm language works.

There's a particularly good comparison of many Redux side effect libraries at What is the right way to do asynchronous operations in Redux?.

All of these are viable tools and approaches for managing side effects in Redux. The two most popular at this point are thunks and sagas, but it really is up to you to decide what you want to use. (For what it's worth, I use thunks and sagas, but haven't needed anything else.)

Dispatching 3-Phase Async Actions

It's pretty common to see Redux applications dispatching multiple actions while making AJAX requests, such as REQUEST_START, REQUEST_SUCCEEDED, and REQUEST_FAILED. This relates to the React world's emphasis on describing as much as possible in terms of explicitly tracked state (which carries over to Redux).

If you want to show some kind of "loading..." spinner, a React app shouldn't just go call $("#loadingSpinner").toggle(). Instead, it should update some kind of state, and use that to determine that it needs to show the spinner. Similarly, with Redux, you're definitely not required to dispatch actions like this when you make AJAX requests, but having those actions and the corresponding state values can be useful for updating the UI or other aspects of the application.

Normalized Data

I covered the main reasons and benefits of normalization in the Structuring Reducers - Normalizing State Shape section of the Redux docs. I'll paste the most relevant section here.

A normalized state shape is an improvement over a nested state structure in several ways:

  • Because each item is only defined in one place, we don't have to try to make changes in multiple places if that item is updated.
  • The reducer logic doesn't have to deal with deep levels of nesting, so it will probably be much simpler.
  • The logic for retrieving or updating a given item is now fairly simple and consistent. Given an item's type and its ID, we can directly look it up in a couple simple steps, without having to dig through other objects to find it.
  • Since each data type is separated, an update like changing the text of a comment would only require new copies of the "comments > byId > comment" portion of the tree. This will generally mean fewer portions of the UI that need to update because their data has changed. In contrast, updating a comment in the original nested shape would have required updating the comment object, the parent post object, the array of all post objects, and likely have caused all of the Post components and Comment components in the UI to re-render themselves.

Note that a normalized state structure generally implies that more components are connected and each component is responsible for looking up its own data, as opposed to a few connected components looking up large amounts of data and passing all that data downwards. As it turns out, having connected parent components simply pass item IDs to connected children is a good pattern for optimizing UI performance in a React Redux application, so keeping state normalized plays a key role in improving performance. (I talked more about how normalization matters for performance in my post Practical Redux, Part 6: Connected Lists, Forms, and Performance.)

Selector Functions

It's perfectly legal to write code that accesses nested portions of the state tree directly, such as const item = state.a.b.c.d. However, the standard software engineering principles of abstraction and encapsulation once again come into play. If looking up certain fields can be complicated, than it's probably a good idea to encapsulate that work in a function. If other parts of the application shouldn't be concerned with the exact structure of the state, or exactly where to find a particular piece of data, it's probably a good idea to encapsulate that lookup process in a function. "Selector functions" are thus simply functions that read in some portion of the state tree and return some subset or derived data.

The use of selector functions with Redux was originally inspired from the idea of "getters" in NuclearJS, which allowed you to subscribe to changes in certain keys of state, and derive data. A similar approach was proposed during Redux's development, and the concept was turned into the Reselect library.

Selector functions can simply be plain functions, but Reselect generates selector functions that can easily use multiple other selector functions as inputs, and memoize them so that the output selector only runs when the inputs change. This is an important factor for performance in two ways. First, expensive filtering or other similar operations in the output selector won't re-run unless needed. Second, because the memoized selector will return the previous result object, it will work nicely with the shallow equality/reference checks in connect (and possibly PureComponent or shouldComponentUpdate).

React-Redux: connect, mapState, and mapDispatch

It's possible to import the store directly into every component file, write the code to have the component subscribe to the store, extract the data it needs whenever the store is updated, and trigger a re-render of the component. None of that process is "magic". But, repeating one of the themes of this post, it's good software engineering to encapsulate the process so that Redux users don't have to deal with writing that repetitive logic themselves.

The React-Redux connect function serves several purposes:

  • It automatically handles the process of subscribing to the store itself
  • It implements the logic for extracting the pieces of state the wrapped component needs
  • It ensures the wrapped component only re-renders when needed
  • It shields the wrapped component from ever actually needing to know that the store exists or that its props are actually coming from Redux

A mapState function is basically a specialized selector function, which always receives the entire state as one argument, may receive the wrapped component's own props as a second argument, and must always return an object. The returned object's contents are, per the name, turned into props for the wrapped component.

A mapDispatch function allows injection of the store's dispatch method, enabling the creation of logic and functions that dispatch actions from the component. If no mapDispatch function is provided, the default behavior is to inject dispatch itself as a prop so the component itself can dispatch actions. Otherwise, the object returned by mapDispatch is also turned into props. Since the most common use case is to wrap up action creators so their returned actions are passed straight to dispatch, connect allows an "object shorthand" syntax - an object full of action creators can be passed instead of an actual mapDispatch function.

If you think about it, connect and the wrapper components it generates act like a lightweight Dependency Injection mechanism (especially via use of React's context mechanism for making the store reference available to nested components). This enables easier testing, since the component isn't reliant on a specific store instance, but also opens up potential advanced use cases like using separate stores in different parts of the component tree, or even overriding/wrapping the store functions that are exposed to nested components.

Summing Up Common Patterns

Overall, these common patterns and approaches generally can be seen as the result of either core Redux design decisions, or straightforward software engineering principles like encapsulation, de-duplication, and reusability. Other than plain action objects, all of these concepts are indeed optional, and if you don't want to use them you don't have to, but there are good reasons why they exist.

Philosophy and Variations in Usage

I've looked at a lot of different Redux-related code. I've read through hundreds of libraries, applications, tutorials, and articles, and I've seen a huge variety of styles, approaches, and implementations. Between that and my status as a Redux maintainer, I think it's fair to say that I'm an expert on how Redux is used. It also means that, as I said in the intro, I Have Opinions about what "good Redux code" looks like, and what qualifies as "idiomatic Redux "code". We've now reached the part where I'm going to express those opinions. (You have been warned :) )

So, in this last section, we'll look at several variations in how Redux can be used, and I'll offer my thoughts on whether these things are or are not in keeping with the spirit of Redux.

Independent Slice Reducers vs All-Updates-Together

As discussed earlier, the primary intended reducer structure is slice reducers composed together. However, since reducers are just functions, there's an infinite variety of ways to write and structure reducer logic. I discussed some alternate approaches in the Structuring Reducers - Beyond combineReducers docs section, including some examples of sharing data between slice reducers, sequencing dependencies, and use of higher-order reducers to wrap existing reducers with additional functionality.

One complaint I've seen is that having many separate slice reducers responding to the same action makes it harder to figure out what actually gets updated for that action. An example of this is the article Problems with Flux, written shortly after Redux was released. A couple quotes:

As a developer, I wish to have an overview of all the state updates an action triggers. In one place in my code. Line by line. Having a comprehensive summary somewhere in the code maximizes the possibility of getting the state update correct, especially if the order of the write operations is relevant!

I believe that Flux as of today splits the store/action relationship matrix on the wrong axis: You can easily see all the actions that affect one part of your state. But it's hard to figure out all the state one action affects. In my opinion, it should be the opposite.

I can see that point, and would say there's some truth to it. One the other hand, one of the reasons to have action constants is that it makes it really easy to "Find All Usages" in your codebase, so it doesn't seem like it's that much more work to see what's happening.

I would say that composed slice reducers still qualifies as more idiomatic, but if you'd rather structure your reducer logic oriented around the actions, that's fine, and another example of "do whatever works for you".

Action Semantics

This is a topic that has resulted in very long, very complicated, and very pedantic discussions, from people who care deeply about this sort of thing (and especially those who have experience with related concepts like Event Sourcing and CQRS). I actually don't have too many opinions in this area, but I'll try to highlight some of the areas of discussion.

Action Tense and "Setters" vs "Events"

There's been considerable debate over what verb tense to use when writing Redux action constants. Past tense, such as "LOADED_THING", can be seen as saying "here is something that occurred, how do you want to respond to it?". Present tense, such as "LOAD_THING", can be seen as more of a command - "go do this". This apparently ties into differences between Event Sourcing and CQRS, which I am only vaguely familiar with.

As a related example, take the idea of buying a pizza combo that gives you a pizza and a bottle of soda. Is it better to dispatch "PIZZA_BOUGHT", or is it better to dispatch "PIZZA_INCREMENT" and "SODA_INCREMENT" ?

In a Twitter thread about issues with thunks, Dan Abramov said:

Indeed “glorified setters” is a very common misuse of Redux.
If your actions creator names start with set* and you often call multiple in a row, you might be missing the point of using Redux.
The point is to decouple “what happened” from “how the state changes”. “Setter” action creators defeat the purpose, conflating the two.

I see Dan's point, and he's not wrong, but there's a whole lot of gray area in here.

On the one hand, you can dispatch an action with whatever contents you want, and if no part of the reducer logic cares about it, it will sail through the system and no state will be updated. On the other hand, there usually is an implicit contract between the code that formats/dispatches the action, and some portion of the reducer logic that is specifically interested in that action. If the action isn't formatted correctly, then the reducer that's looking for that specific action type will either ignore it, or try to access a non-existent field and break.

In that sense, Redux is just like any other event-based / pub-sub style system. Triggering events or dispatching actions acts like a "function call at a distance", and if you don't provide the right parameters to whatever code is on the other end, it won't work.

Following on from that, there usually is an expectation that a certain chunk of the reducer logic is interested in this specific action. For example, if you have listA, listB, and listC in your state, and each of those is managed by copies of the same slice reducer function, dispatching "LIST_ITEM_INSERT" needs some kind of additional info to differentiate which list it's supposed to apply to, whether it be dispatching type "b/LIST_ITEM_INSERT" instead, or adding listId : "b" to the action. We frequently toss out the idea of "treating the store like it's a client-side database", and certainly an SQL update query would normally give specifics on what rows should be updated.

Ultimately, I don't think there is an absolute "right" answer here. I think this is a topic where it's really easy to get caught up in bikeshedding, and I'd rather spend that time building something useful :)

Multiple Dispatches

This topic carries on from the previous one.

In the couple applications I've worked on, I've found myself putting together a number of generic "primitive actions" that are usually dispatched as part of a larger sequence. In Practical Redux, Part 8: Form Draft Data Management, the logic for "stop editing a Pilot and save the changes" involves dispatching three actions: a specific "PILOT_EDIT_STOP" action that resets a flag, a generic "apply changes from draft to current for this item type+ID" action, and a generic "delete this item type+ID from draft" action. I certainly could handle all three steps in a single action, but as I was putting together the logic it made more sense to build the primitives first and then compose them together, especially because I needed to repeat the "draft data" behavior multiple times for any kind of item.

I addressed some of the concerns regarding multiple dispatches in my post Idiomatic Redux: Thoughts on Thunks, Sagas, Abstraction, and Reusability. Quoting myself:

In the end, I'm not worried about these concerns. If perf is an issue with dispatching, it can be handled with one of the various batched dispatch approaches. If your actions can result in some "unstable" states, you probably want to rethink how that's being handled anyway (or, apply the same batching concepts to implement "transactions").

Reducer Variations

Cursors and "All-In-One" Reducers

A "cursor" is (roughly) a function that offers read/write access to a particular nested piece of data. For example, if I had a notional cursor library and created a cursor like: const nestedCursor = cursor(state).refine(["a", "b", "c"]), the returned cursor object would let me manipulate state.a.b.c without having to worry about the intermediate layers myself.

A number of people have tried to apply that approach to updating Redux state, either directly through an actual cursor library, or via reducers that allow you to specify a state keypath and new value in the action. I've also seen people try to write "simplified reducers", where the only action in the entire application is along the lines of "SET_DATA", and the only reducer function is simply return {...state, ...action.payload}.

There was an extended discussion on how Redux relates to cursors in redux#155. Dan had some great comments in that thread, and actually anticipated people trying these approaches. I'll quote some of his comments here.

First, he describes why Redux avoids write cursors:

You can implement read-only cursors on top of Redux very easily. Just listen to the root changes, select a specific path and compare if the reference has changed since the last time. What Redux does not give you is write cursors. This is a core design decision made for a reason.

Redux lets you manage your state using composition. Data never lives without a reducer ("store" in current docs) that manages that data. This way, if the data is wrong, it is always traceable who changed it. It is also always possible to trace which action changed the data.

With write cursors, you have no such guarantees. Many parts of code may reference the same path via cursor and update it if they want to.

Later on, he addressed the idea of "SET_DATA"-style reducers:

Do you see that as a philosophical/pattern stance or as a technical one? In other words, is Redux somehow preventing write cursors or otherwise causing the developer to fall into the pit of success?

For example, if I created a single action set that took a path as a parameter and passed that around to my components, would I have implemented the same thing as a write cursor? Or would that somehow still be fundamentally different than a cursor?

That's a great question! You can totally do that.

What I'm saying is that cursors are very low-level API. Just like you can have a single React component for your whole application, you can have a single SET action and a single reducer that behaves akin to a cursor. In my experience it's not very practical but you can definitely do this.

One important difference is that your SET action will still flow through Redux's dispatcher which potentially allows us to implement things like time travel (#113) and logging/other middleware (#63) that stands between your action and the actual data. With a vanilla cursor approach, a framework just doesn't have the power to do something like that.

Both of these are things that are certainly possible with Redux, but do go against the intended spirit of "semantically meaningful actions that can be traced through the system".

Thick and Thin Reducers

As discussed in the Redux FAQ entry on "where do I put business logic?", there's valid tradeoffs with putting more logic in action creators vs putting more logic in reducers. One good point that I saw recently is that if you have more logic in reducers, that means more things that can be re-run if you are time-travel debugging (which would generally be a good thing).

I personally tend to put logic in both places at once. I write action creators that take time to determine if an action should be dispatched, and if so, what the contents should be. However, I also often write corresponding reducers that look at the contents of the action and perform some complex state updates in response.

I generally try to minimize the number of places I do return {...state, ...action.payload}. That approach is definitely helpful if I'm doing something like updating multiple possible fields in a form and don't want to write separate updateName / updateAge / updateWhatever handlers for each field.

I would say neither is more "idiomatic" specifically and are perfectly valid choices, but there are some benefits of erring on the side of more logic in reducers.

Reducers In Actions

I've seen several cases where people put "reducers into their actions". The code that dispatches the action includes a reducer or state update function, and the root reducer simply calls return action.reducer(state). This seems like a bad idea for a couple reasons. It not only loses traceability in the same way as write cursors, but also will break time-travel debugging because the functions won't serialize properly.

OOP and FP Variations

As discussed throughout these two posts, the core values of Redux are things like:

  • dispatch plain action objects
  • actions and state should be serializable to enable time-travel and persistence
  • use pure functions and plain data
  • there's no real need for classes anywhere
  • reducers should be able to independently listen to the same actions and respond appropriately.

Functional programming can be a tough concept to adjust to for many programmers that only have experience with OOP. (I myself am still somewhere in the middle of that spectrum - I'm fine with basic use of FP, but high-level FP usage is still mostly over my head, and there's aspects of OOP that I still find more comfortable.)

As a result, some people have tried to build various OOP layers on top of Redux. This includes libraries like Radical, React-Redux-OOP, Tango, Conventional-Redux, and many others. These libraries tend to follow similar patterns - things like defining classes whose methods are action creators and reducers, domain models to insert into the Redux store, "state classes" that wrap around state values, or "dispatching functions" instead of plain action objects.

These libraries generally wrap up and hide aspects of Redux's behavior, mostly for the sake of doing things in an OOP fashion. A common theme is that these libraries only support a 1:1 relationship between dispatched actions and reducers, when many reducers responding to one action is a core intended use of Redux.

There's an excellent slideshow called Reactive, Component-Based UIs that lays out some principles for "Thinking in React", and I think these principles totally apply to Redux as well. Quoting slide 3:

That is, while there is value in the items on the right, we value the items on the left more."

  • Functional over OO
  • Stateless over Stateful
  • Clarity over Brevity

It's also worth noting that any framework or library has certain idioms and expectations for how code is written and structured. When you start writing code that goes against those idioms, there are fewer people who are going to be familiar with your approach, and it's less likely that it will pick up any traction. I wrote an HN comment a while back about why a particular OOP Redux wrapper library wasn't getting attention, and went into more detail on that topic.

Overall, these OOP wrapper patterns may work at the technical level, but they definitely go against the intent and spirit of Redux.

Final Thoughts

There's an apocryphal story about an experiment where scientists taught monkeys to not climb up a ladder by spraying water at them, and later on, older monkeys would slap newer monkeys who tried to climb the ladder. Searching suggests that story is fake, but we understand the moral involved - many times people are told to do things without understanding why, and it becomes "received wisdom" that is passed down the chain. Eventually someone complains about the situation thinking the behavior is pointless, and it's because they don't understand the original reasoning for doing things that way in the first place.

I'd say that a lot of people's complaints about Redux follow this sort of category. They've seen "action creators" and "immutability" and folders named containers and dozens of different side effect middlewares, complain about the apparent excess baggage involved in using Redux, and "why do I need any of this stuff in the first place?".

Well, turns out that if you look at the history of Redux and how it's intended to be used, and then apply some pretty straightforward software engineering principles to its use in larger applications, you wind up with the common patterns and practices that we now can identify as "idiomatic Redux usage": plain action objects and action creators, slice reducers and switch statements, async middleware and connected components, selectors and normalized state. (For a great example of how these pieces fit together in practice, check out the recent article by Mapbox on Redux for state management in large apps.)

As I've said throughout these posts, in the Redux FAQ, and in comments on Reddit and HN and elsewhere: ultimately, it's your application, and your codebase. If you don't like these patterns, you don't have to use them. Redux is simple and flexible enough that you can twist it in a myriad of other "unintended" directions. But, like Chesterton's fence, you should at least understand that these idiomatic usage patterns exist for good reasons!

I hope this journey through the history and usage of Redux has been informative, and helps you and your team go forth and build better applications.

Until next time, Happy Redux-ing!

Further Information


This is a post in the Idiomatic Redux series. Other posts in this series:


Author Avatar

Mark Erikson

Collector of interesting links, answerer of questions