Movatterモバイル変換


[0]ホーム

URL:


Mark's Dev Blog

Random musings on React, Redux, and more, by Redux maintainer Mark "acemarke" Erikson
 Sponsor @markerikson
 Home

Idiomatic Redux: The Tao of Redux, Part 1 - Implementation and Intent

Posted on
#javascript#redux#functional#oop#abstraction#greatest-hits

This is a post in theIdiomatic Redux series.


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 arejust opinions, but I'd like to think they're pretty good approaches to follow :)

Redux is, at its core, an incredibly simple pattern. It saves a current value, runs a single function to update that value when needed, and notifies any subscribers that something has changed.

Despite that simplicity, or perhapsbecause of it, there's a wide variety of approaches, opinions, and attitudes about how to use Redux. Many of these approaches diverge widely from the concepts and examples that are in the docs.

At the same time, there's been ongoing complaints about how Redux "forces" you to do things certain ways. Many of the complaints actually involve concepts related to how Redux is typically used, rather than any actual limitation imposed by the Redux library itself. (For example, in one recent HN thread alone, I saw complaints about "too much boilerplate", "action constants and action creators aren't needed", "I have to edit too many files to add a feature", "why do I have to switch files to get to my write logic?", "the terms and names are too hard to learn or are confusing", andway too much more.)

As I've researched, read, discussed, and examined the variety of ways that Redux is used and the ideas being shared in the community, I've concluded thatit's important to distinguish between how Reduxactually works, the ways that Redux isintended to be used conceptually, and the nearly infinite number of ways that it'spossible to use Redux. I'd like to address several aspects of Redux usage, and discuss how they fit into these categories. Overall,I hope 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.

This post will be split into two parts. InPart 1 - Implementation and Intent, we'll look at the actual implementation of Redux, what specific limitations and constraints it requires, and why those limitations exist. Then, we'll review the original intent and design goals for Redux, based on the discussions and statements from the authors (especially during the early development process).

InPart 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 arepossible but not necessarily "idiomatic".

Table of Contents 🔗︎

Laying the Foundation 🔗︎

Examining the Three Principles 🔗︎

Let's start by taking a look at the now-famousThree Principles of Redux:

  • Single source of truth: The state of your whole application is stored in an object tree within a single store.
  • State is read-only: The only way to change the state is to emit an action, an object describing what happened.
  • Changes are made with pure functions: To specify how the state tree is transformed by actions, you write pure reducers.

In a very real sense,each one of those statements is a lie! (Or, to borrow the classic line fromReturn of the Jedi, "they're true... from a certain point of view.")

So, if these statements aren't entirely true, why even have them?These principles aren't fixed rules or literal statements about the implementation of Redux. Rather, they forma statement of intent about how Reduxshould be used.

That theme is going to continue throughout the rest of this discussion. Because Redux is such a minimal library implementation-wise, there's very little that it actually requires or enforces at the technical level. That brings up a valuable side discussion that's worth looking at.

"Language" and "Meta Language" 🔗︎

In Cheng Lou'sReactConf 2017 talk on "Taming the Meta Language", he described howonly source code is "language", and everything else, like comments, tests, docs, tutorials, blog posts, and conferences, is "meta language". In other words, the source code itself can only convey a certain amount of information by itself. Many additional layers of human-level information passing are needed in order to help people understand the "language".

Cheng Lou's talk then continues on to discuss how moving additional concepts into the actual programming language itself enable expressing more information through the medium of the source code, without having to resort to the use of "meta language" to pass on the ideas. From that perspective,Redux is a tiny "language", and almost all of the information abouthow it should be used is actually "meta language".

The "language" (in this case, the core Redux library) has minimal expressivity, and therefore the concepts, norms, and ideas that surround Redux are all at the "meta language" level. (In fact, the postUnderstanding "Taming the Meta Language", which breaks down the ideas in Cheng Lou's talk, actually calls out Redux as a specific example of these ideas.) Ultimately, what this means is thatunderstanding why certain practices exist around Redux, and decisions of what is and isn't "idiomatic", will involve opinions and discussions rather than just determination based on the source code.

How Redux Actually Works 🔗︎

Before we really get much further into the philosophical side of things, it's important to understand what kind of technical expectations Reduxdoes actually have. Taking a look at the internals and implementation is informative.

The Core of Redux:createStore 🔗︎

ThecreateStore function is the core of Redux's functionality. If we strip out the comments, the error checking, and the code for a couple of advanced features like store enhancers and observables, here's whatcreateStore looks like (code sample borrowed from a "build-a-mini-Redux" tutorial calledHacking Redux ):

function createStore(reducer) {    var state;    var listeners = []    function getState() {        return state    }        function subscribe(listener) {        listeners.push(listener)        return function unsubscribe() {            var index = listeners.indexOf(listener)            listeners.splice(index, 1)        }    }        function dispatch(action) {        state = reducer(state, action)        listeners.forEach(listener => listener())    }    dispatch({})    return { dispatch, subscribe, getState }}

That's approximately 25 lines of code, yet it includes the key functionality. It tracks the current state value and multiple subscribers, updates the value and notifies subscribers when an action is dispatched, and exposes the store API.

Consider all the things this snippetdoesn't include:

In that vein, it's worth quotingDan Abramov's pull request for the "counter-vanilla" example:

The new Counter Vanilla example is aimed to dispel the myth that Redux requires Webpack, React, hot reloading, sagas, action creators, constants, Babel, npm, CSS modules, decorators, fluent Latin, an Egghead subscription, a PhD, or an Exceeds Expectations O.W.L. level. Nope, it's just HTML, some artisanal script tags, and plain old DOM manipulation. Enjoy!

Thedispatch function inside ofcreateStore simply calls the reducer function and saves whatever value it returns. And yet, despite that, the items in this list of ideas are widely acknowledged to be concepts that a good Redux app should care about.

Having listed all the things thatcreateStore doesnot care about, it's important to note what itdoes actually require. The realcreateStore function enforces two specific limitations:actions that reach the store must be plain objects, andactions must have atype field that is not undefined.

Both of these constraints have their origins in the original "Flux Architecture" concept. To quote theFlux Actions and the Dispatcher section of the Flux docs:

When new data enters the system, whether through a person interacting with the application or through a web api call, that data is packaged into an action — an object literal containing the new fields of data and a specific action type. We often create a library of helper methods called ActionCreators that not only create the action object, but also pass the action to the dispatcher.

Different actions are identified by a type attribute. When all of the stores receive the action, they typically use this attribute to determine if and how they should respond to it. In a Flux application, both stores and views control themselves; they are not acted upon by external objects. Actions flow into the stores through the callbacks they define and register, not through setter methods.

Redux did not originally require thetype field specifically, but the validation check was later added to help catch possible typos or wrong imports of action constants, and to avoid bikeshedding regarding the basic structure of action objects.

The Built-In Utility:combineReducers 🔗︎

This is where we start seeing some constraints that more people are familiar with.combineReducers expects that each slice reducer it's been given will "correctly" respond to an unknown action by returning its default state, and never actually returnundefined. It also expects that the current state value is a plain JS object, and that there's an exact correspondence between the keys in the current state object and the reducer functions object. Finally, it does reference equality comparisons to see if all slice reducers returned their previous values. If all of the returned values appear to be the same, it assumes that nothing actually changed anywhere, and it returns the original root state object as a potential optimization.

The Original Selling Point: Redux DevTools 🔗︎

The Redux DevTools consists of two main pieces: the store enhancer that implements the time traveling behavior by tracking a list of dispatched actions, and the UI that allows you to view and manipulate the history. The store enhancer itself does not care about the contents of the actions or the state - it just stores the actions in memory. The original DevTools UI is a component you render inside of your application's component tree, and it also does not care about the contents of your actions or state. However, the Redux DevTools Extension operates in a separate process (at least under Chrome), and thus requires all actions and state to be serializable in order for all time-traveling features to behave properly and performantly. The ability to import and export state and actions also requires them to be serializable as well.

The other semi-requirement for time travel debugging is immutability and pure functions.If a reducer function mutates state, then jumping between actions in the debugger will result in inconsistent values. If a reducer has side effects, then those side effects will be re-executed each time the action is replayed by the DevTools. In either case, time-travel debugging won't fully work as expected.

The Main UI Bindings: React-Redux andconnect 🔗︎

React-Redux'sconnect function is where mutation really becomes an issue. The wrapper components generated byconnect implement a lot of optimizations to ensure that the wrapped components only re-render when actually necessary. Those optimizations revolve around reference equality checks to determine if data has actually changed.

Specifically, every time an action is dispatched and subscribers are notified,connect checks to see if the root state object has changed. If it hasn't,connect assumes that nothing else in the state changed, and skips any further rendering work. (This is whycombineReducers tries to return the same root state object if possible.) If the root state objectdid change,connect will call the suppliedmapStateToProps function, and do a shallow equality check on the current result versus the previous returned result, to see if any of the props from store data have changed. Again, if thecontents of the data appears to be the same,connect will not actually re-render the wrapped component.These equality checks inconnect are why accidental state mutations result in components not re-rendering, becauseconnect assumes that data hasn't changed and re-rendering isn't needed.

Commonly Paired Libraries: React and Reselect 🔗︎

Immutability comes into play in other libraries that are commonly used with Redux as well. The Reselect library creates memoized "selector" functions that are typically used to extract data from the Redux state tree. Memoization normally relies on reference equality checks to determine if the input parameters are the same.

Similarly, while a React component can implementshouldComponentUpdate using any logic it wants, the most common implementation relies on shallow equality checks of the current props and incoming props, such asreturn !shallowEqual(this.props, nextProps).

In either case, mutation of data would generally result in undesired behavior. Memoized selectors would likely not return the proper values, and optimized React components would not re-render when they actually should.

Summarizing Redux's Technical Requirements 🔗︎

The core ReduxcreateStore function itself puts only two limitations on how you must write your code: actions must be plain objects, and they must contain a definedtype field. It does not care about immutability, serializability, or side effects, or what the value of thetype field actually is.

That said,the commonly used pieces around that core, including the Redux DevTools, React-Redux, React, and Reselect,do rely on proper use of immutability, serializable actions/state, and pure reducer functions. The main application logic may work okay if these expectations are ignored, but it's very likely that time-travel debugging and component re-rendering will break. These also will affect any other persistence-related use cases as well.

It's also important to note thatimmutability, serializability, and pure functions are not enforced in any way by Redux. It's entirely possible for a reducer function to mutate its state or trigger an AJAX call. It's entirely possible for any other part of the application to callgetState() and modify the contents of the state tree directly. It's entirely possible to put promises, functions, Symbols, class instances, or other non-serializable values into actions or the state tree. You are notsupposed to do any of those things, but it'spossible.

The Intent and Design of Redux 🔗︎

With those technical constraints in mind, we can turn our attention to how Redux isintended to be used. To better understand that intent, it's helpful to look back at the ideas and influences that drove the initial development of Redux.

Redux's Influences and Goals 🔗︎

The "Introduction" section in the Redux docs lays out several major influences on Redux's development and concepts in theMotivation,Core Concepts, andPrior Art topics. As a quick summary:

It's also worth taking a look at the stated design goals froman early version of the Redux README

Philosophy & Design Goals

  • You shouldn't need a book on functional programming to use Redux.
  • Everything (Stores, Action Creators, configuration) is hot reloadable.
  • Preserves the benefits of Flux, but adds other nice properties thanks to its functional nature.
  • Prevents some of the anti-patterns common in Flux code.
  • Works great in isomorphic apps because it doesn't use singletons and the data can be rehydrated.
  • Doesn't care how you store your data: you may use JS objects, arrays, ImmutableJS, etc.
  • Under the hood, it keeps all your data in a tree, but you don't need to think about it.
  • Lets you efficiently subscribe to finer-grained updates than individual Stores.
  • Provides hooks for powerful devtools (e.g. time travel, record/replay) to be implementable without user buy-in.
  • Provides extension points so it's easy tosupport promises orgenerate constants outside the core.
  • No wrapper calls in your stores and actions. Your stuff is your stuff.
  • It's super easy to test things in isolation without mocks.
  • You can use “flat” Stores, orcompose and reuse Stores just like you compose Components.
  • The API surface area is minimal.
  • Have I mentioned hot reloading yet?

Design Principles and Intent 🔗︎

Reading through the Redux docs, the early Redux issue threads, and many other comments made by Dan Abramov and Andrew Clark elsewhere, we can see several specific themes regarding theintended design and use of Redux.

Redux Was Built As A Flux Architecture Implementation 🔗︎

Redux was originally intended to be "just" another library that implemented the Flux Architecture. As a result, it inherited many concepts from Flux: the idea of "dispatching actions", that actions are plain objects with atype field, the use of "action creator functions" to create those action objects, that "update logic" should be decoupled from the rest of the application and centralized, and more.

I frequently see questions asking "Why does Redux do $THING?", and for many of those questions the answer is "Because that's how the Flux Architecture and specific Flux libraries did things".

State Update Maintainability Is The Main Priority 🔗︎

Almost every aspect of Redux is meant to make it easier for a developer to understand when, why, and how a given piece of state changed. That includes both actual implementation as well as encouraged usage.

That means that a developer should be able to look at a dispatched action, see what state changes occurred as a result, and trace back to the places in the codebase where that action is dispatched (especially based on the type of the action). If data is wrong in the Redux store, it should be possible to trace what dispatched action resulted in that wrong state, and work backwards from there.

The emphasis on "hot reloading" and "time-travel debugging" is also aimed squarely at developer productivity and maintainability, since both of those allow a developer to iterate faster and better understand what's happening in the system.

Action History Should Have Semantic Meaning 🔗︎

While Redux's core does not care what the actual value of your action'stype field is, the clear intent is that action typesshould have some kind of meaning and information. The Redux DevTools and other logging utilities display thetype field for each dispatched action, so having values that are understandable at a quick glance is important.

This means that strings are more useful than Symbols or numbers in terms of conveying information. It also means that the wording of those action type strings should be clear and understandable. This generally means that having more distinct action types is going to be better for developer understanding than only having one or two action types. If only a single action type is used across the entire codebase (likeSET_DATA), it will be harder to track down where a particular action was dispatched from, and the history log will be less readable.

Redux Is Intended To Introduce Functional Programming Principles 🔗︎

Redux is explicitly intended to be built and used with functional programming concepts, and to help introduce those concepts to both new and experienced developers. This includes FP basics such as immutability and pure functions, but also ideas such as composing functions together to achieve a larger task.

At the same time, Redux is intended to help provide real value to developers trying to solve problems and build applications, without overwhelming a user in too many abstract FP concepts or getting bogged down in arguments over deep FP terminology like "monads" or "endofunctors". (Admittedly, the number of terms and concepts around Redux has grown over time, and many of thoseare confusing to new learners, but the goals of leveraging the benefits of FP and introducing learners to FP were clearly part of the original design and philosophy.)

Redux Promotes Testable Code 🔗︎

Having reducers be pure functions enables time travel debugging, but it also means that a reducer function should be easily testable in isolation. Testing a reducer should only require calling it with specific arguments, and verifying the output - no need to mock things like AJAX calls.

AJAX calls and other side effects still have to live somewhere in the application, and testing code that uses those can still take work. However, emphasizing pure functions for a meaningful part of the codebase reduces the overall complexity of testing.

Reducer Functions Should Be Organized By State Slice 🔗︎

Redux takes the concept of individual "stores" from the Flux architecture, and merges them into a single combined store. The most straightforward mapping between Flux and Redux is to create a separate top-level key or "slice" in the state tree for each store. If a Flux app has a separate UsersStore, PostsStore, and CommentsStore, the Redux equivalent would probably have a root state tree that looks like{users, posts, comments}.

It's possible to have a single function that contains all the logic for updating all of those state slices together, but any meaningful application will want to break up that function into smaller functions for maintainability. The most obvious way to do that is to split up the logic based on which slice of state needs to be updated. This means that each "slice reducer" only needs to worry about its own slice of state, and as far as it knows, that slice may as well beall of the state. This pattern of "reducer composition" can be nested repeatedly to handle updates to nested state structure, and thecombineReducers utility is included with Redux specifically to make it easy to follow this pattern.

If each slice reducer function can be called separately and given just its own slice of state as a parameter, that also implies that multiple slice reducers can be called with the same action, and each one can update its own slice of state independently from any others. Based on statements from Dan and Andrew,having a single action result in updates from multiple slice reducers is a core intended use case for Redux. This is often referred to as actions having a "1:many" relationship with reducer functions.

Update Logic And Data Flow Are Explicit 🔗︎

Redux does not contain any "magic". A few aspects of its implementation are a bit tricky to grasp right away unless you're familiar with some more advanced FP principles (such asapplyMiddleware and store enhancers), but otherwise everything is intended to be explicit, clear, and traceable, with minimal abstraction.

Redux really doesn't even implement the actual state update logic. It simply relies on whatever root reducer functionyou provide. It does provide thecombineReducers utility to help with the intended common use case of slice reducers managing state independently, but you are entirely encouraged to write your own reducer logic to handle your own needs.This also means that your reducer logic can be simple or complex, abstracted or verbose - it's all about howyou want to write it.

In the original Flux dispatcher, Stores needed awaitFor() event that could be used to set up dependency chains. If a CommentsStore needed data from a PostsStore to properly update itself, it could callPostsStore.waitFor() to ensure that it would run after the PostsStore updated. Unfortunately, that dependency chain was not easily visualized. However, with Redux, that sequencing can simply be accomplished by explicitly calling specific reducer functions in sequence.

As an example, here's some (slightly modified) quotes and snippets from Dan's"Combining Stateless Stores" gist:

In this casecommentsReducer no longer truly depends on the state and action. It also depends onhasCommentReallyBeenAdded.

We add this parameter to its API. Sure, it's no longer usable “as is”, but that's the point: it has an explicit dependency now on other data. It's not a top-level store. Whoever manages it must somehow give it that data.

export default function commentsReducer(state = initialState, action, hasPostReallyBeenAdded) {}// elsewhereexport default function rootReducer(state = initialState, action) {  const postState = postsReducer(state.post, action);  const {hasPostReallyBeenAdded} = postState;  const commentState  = commentsReducer(state.comments, action, hasPostReallyBeenAdded);  return { post : postState, comments : commentState };}

This also applies to the idea of "higher order reducers". A given slice reducer can be wrapped up in other reducers to add abilities like undo/redo or pagination.

Redux's API Should Be Minimal 🔗︎

This goal was stated repeatedly by both Dan and Andrew throughout Redux's development. It's easiest to just quote some of their comments:

Andrew - #195:

The best API is often no API. The current proposals for middleware and higher-order stores have the tremendous benefit that they require no special treatment by the Redux core — they're just wrappers arounddispatch() andcreateStore(), respectively. You can even use them today, before 1.0 is released. That's a huge win for extensibility and rapid innovation. We should favor patterns and conventions over rigid, privileged APIs.

Dan - #216:

Here's why I chose to write Redux instead of using NuclearJS:

  • I don't want a hard dependency on ImmutableJS
  • I want as little API as possible
  • I want to make it easy to jump off Redux when something better comes around

With Redux, I can use plain objects, arrays and whatnot for the state.

I tried hard to avoid APIs likecreateStore because they bind you to a particular implementation. Instead, for each entity (Reducer, Action Creator) I tried to find the minimal way to expose it without having any dependency on Redux whatsoever. The only code importing Redux and actually depending hard on it will be in your root component and the components that subscribe to it.

Redux Should Be As Extensible As Possible 🔗︎

This ties in with the "minimal API" goal. Some Flux libraries, like Andrew's Flummox lib, had some form of async behavior built-in to the library itself (such as dispatchingSTART/SUCCESS/FAILURE actions for promises). However, while having something built into the core meant it was always available, it also limited flexibility.

Again, it's easiest to quote comments from the design discussions and theHashnode AMA with Dan and Andrew :

Andrew - #55:

To continue supporting async actions, and to provide an extensibility point for external plugins and tools, we can provide some common action middleware, a helper for composing middleware, and documentation for how extension authors can easily create their own.

Andrew - #215:

I agree it's a natural feature that most Redux apps will probably want to have, but once we put it into the core, everyone will start bikeshedding exactly how it should work, which is what happened for me with Flummox. We're trying to keep the core as minimal and flexible as possible so we can iterate quickly and allow others to build on top of it.

As Dan said once, (I can't remember where... probably Slack) we're aiming to be like the Koa of Flux libraries. Eventually, once the community is more mature, the plan is to maintain a collection of "blessed" plugins and extensions, possibly under a reduxjs GitHub organization.

Dan - Hashnode AMA:

We didn’t want to prescribe something like this in Redux itself because we know a lot of people are not comfortable with learning Rx operators to do basic async stuff. It’s beneficial when your async logic is complex, but we didn’t really want to force every Redux user to learn Rx, so we intentionally kept middleware more flexible.

Andrew - Hashnode AMA:

[the] reason the middleware API exists in the first place is because we explicitly did not want to prescribe a particular solution for async." My previous Flux library, Flummox, had what was essentially a promise middleware built in. It was convenient for some, but because it was built in, you couldn't change or opt-out of its behavior. With Redux, we knew that the community would come up with a multitude of better async solutions that whatever we could have built in ourselves.

Redux Thunk is promoted in the docs because it's the absolute bare minimum solution. We were confident that the community would come up with something different and/or better. We were right!

Final Thoughts 🔗︎

I spent a lot of time doing research for these two posts. It was fascinating to read back through the early issues and discussions, and watch Redux evolve into what we now know. As seen in that quoted README, the vision for Redux was clear from the beginning, and there were several specific insights and conceptual leaps that resulted in the final API and implementation. Hopefully this look at the internals and the history of Redux helps shed some light on how Redux actually works, and why it was built this way.

Be sure to check outThe Tao of Redux, Part 2 - Practice and Philosophy, where we'll look at why many common patterns of Redux usage exist, and I'll give my thoughts on the pros and cons of many "variations" in how it's possible to use Redux.

Further Information 🔗︎


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

Recent Posts

Presentations: The State of React and the Community in 2025The State of React and the Community in 2025Presentations: Maintaining a Library and a CommunityReact Advanced 2024: Designing Effective DocumentationReact Summit 2024: Why Use Redux Today?

Top Tags

63 redux56 javascript49 react31 presentation30 greatest-hits

Greatest Hits

Greatest Hits: The Most Popular and Most Useful Posts I've WrittenRedux - Not Dead Yet!Why React Context is Not a "State Management" Tool (and Doesn't Replace Redux)A (Mostly) Complete Guide to React Rendering BehaviorPresentations: Modern Redux with Redux ToolkitWhen (and when not) to reach for ReduxThe Tao of Redux, Part 1 - Implementation and IntentThe History and Implementation of React-ReduxThoughts on React Hooks, Redux, and Separation of ConcernsReact Boston 2019: Hooks HOCs, and TradeoffsUsing Git for Version Control Effectively

Series

21 Blogged Answers4 Codebase Conversion4 Coding Career Advice2 Declaratively Rendering Earth In 3d5 How Web Apps Work8 Idiomatic Redux6 Newsletter13 Practical Redux32 Presentations5 Site Administrivia

[8]ページ先頭

©2009-2025 Movatter.jp