Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request for Discussion: Redux "boilerplate", learning curve, abstraction, and opinionatedness #2295

Closed
markerikson opened this issue Mar 19, 2017 · 109 comments

Comments

@markerikson
Copy link
Contributor

markerikson commented Mar 19, 2017

Resolution: use Redux Toolkit

The ideas in this thread eventually turned into our official Redux Toolkit package. It includes utilities to simplify many common Redux use cases, including store setup, reducer definition, immutable update logic, creating entire "slices" of state automatically without writing any action creators or action types by hand, and even automatic data fetching and caching with the "RTK Query" API.

For more details on what problems Redux Toolkit is meant to solve, see the "Vision for Redux Toolkit" manifesto I wrote .

The number one complaint I see about Redux is that there's "too much boilerplate". I also frequently see complaints that there's too much to learn, too many other addons that are needed to do anything useful, and too many aspects where Redux doesn't have any opinion and therefore doesn't offer any kind of built-in guidance.

I just had a discussion with @tannerlinsley regarding several of these aspects. We discussed the Jumpstate library, which is an abstraction layer around Redux, and how it was intended to ease the learning curve, as well as various philosophical opinions about what constitutes "good" Redux usage.

Out of that, we came up with several questions that I would like to bring up for wider discussion:

Key points

Boilerplate / Verbosity

  • Redux is not intended to be the "most concise way of doing things", but rather to make data flow obvious and readable
  • Docs are written in a deliberately verbose style for clarity and learning, and not specifically intended as "the one true way to write Redux code", but that style has been somewhat blindly adopted (or, sometimes, resulted in rejecting Redux)
  • Frequent complaints about "boilerplate", such as "too many files", "use of action creators", etc

Abstractions and Learning

  • The core Redux library itself is effectively feature-complete, but there's lots of interesting addons and tools being built by the community
  • Various abstraction libs have been built to "ease learning curve" or "make things more OOP", but most of these are not really "idiomatic" Redux usage
  • Redux learning curve can be steep, but once you grasp concepts, the need for abstraction layers often goes away

Problems

  • What are the "boilerplate" complaints mostly about?
  • What are the hardest aspects of Redux for new learners?
  • What are the "no opinions" areas that cause problems for people?

Potential Solutions

  • What would idiomatic Redux usage with "less boilerplate" look like? How can we provide solutions to those complaints?
  • What possible abstractions could be created that simplify the process of learning and usage, but without actually hiding Redux (and would hopefully provide a migration/learning path to "base" Redux)?
  • How much of this could be solved with improved docs in some way?

I would like to invite the community to offer up complaints, pain points, and concerns about using Redux, and hopefully also provide suggestions and ideas for solving those problems as well.

@markerikson markerikson changed the title Discussion: Redux "boilerplate", learning curve, abstraction, and opinionatedness Request for Discussion: Redux "boilerplate", learning curve, abstraction, and opinionatedness Mar 19, 2017
@modernserf
Copy link

A few ideas:

  • official redux-preset package containing redux, react-redux, redux-thunk already wired together
  • dispatch(actionType, payload) could reduce need for action creators
  • built-in jumpstate-style reducer creators, e.g. createReducer({[actionType]: (state, payload) => state}, initState)

@e-schultz
Copy link

Most people I know who use redux heavily, end up moving away from redux-thunk as they find it doesn't scale very well, and ends up leading to action creators that start doing way too much, and rather use something else for managing side-effects like redux-observable or redux-saga.

I can understand the appeal in it, especially when getting started with redux - but I'm not sure if bundling it as part of a 'best practice boilerplate package' would be the best idea.

@Phoenixmatrix
Copy link

So, this part is the more critical to me (this will not so much be ideas to solve the problem, but rather constraints that are, at least to me, important):

Redux is not intended to be the "most concise way of doing things", but rather to make data flow obvious and readable

A great thing about Redux is how it's almost more of a design pattern than a framework, and all the code you see (aside for the store and react-redux) is your own.

Looking at Jumpstate (I did not know about it until now), it's one of the first abstraction layers I see for Redux that looks good. Great even! But at the same time, it brings you only a hair away from other frameworks like MobX, and having multiple frameworks that bring in the same thing to the table isn't super useful. So focusing on what makes Redux different from the rest is important, not just how we can make Redux better in a vacuum (because it lives in the same world as other tools).

Another thing of importance, is that a lot of challenges newcomers have when they hit Redux isn't Redux itself, but JavaScript. Other big frameworks like Angular abstract away JavaScript itself. Writing a Redux reducer is simply applying vanilla javascript "design patterns", which may not be familiar to newcomers, and will make people want to use a black box instead.

Finally, a lot of boilerplate reduction libraries try to add a 1:1:1 relationship between components, reducers and action creators (maybe not all 3, but often 2 of those), which makes newcomers forget they can have multiple reducers handling an action, multiple components using state from multiple reducers, and so on. That's one of the #1 question I answer at work and other places, and it's super useful. So tool to help in that area should not lose this (also, "switch are icky" isn't the best argument in the world).

So IMO, CTAs would involve linking good vanilla JavaScript resources, as well as documenting more of the "why" right along the "getting started" concepts, and keeping things simple. You can build gigantic applications in Redux without learning many new concepts at all. While I'm a redux-observable guy myself, I've seen multi-hundred-thousand lines of code apps using Thunk without issues (and Ive seen tiny apps make a trainwreck with thunks). Introducing very few "core" concepts and showing how they can be applied to tons of concepts helps a lot.

The "all boilerplates must be reduced no matter the cost" is an issue with the software engineering community as a whole these days...thats harder to tackle.

@alex35mil
Copy link
Contributor

alex35mil commented Mar 19, 2017

From the very beginning my main concern w/ redux was that either I read or write a code, I had to jump between N files, b/c logic of the single UI part is scattered all over the codebase between actions, action types and several reducers. I really like that I can reach out to any part of the state tree from every place in UI and I can change the different parts of the state in response to a single action (main reasons why I use redux), but the more parts of the state I change in response to a single action, the more my logic is blured. I can't simply read or write what's happened when user did this or that. But what I want is can be described like this:

// meta code
dispatch(ACTION);

onAction = {
  ACTION: [
    // handler 1: hide spinner here,
    // handler 2: change status there,
    // handler 3: update entity
  ],
};

In the end I came up with redux-interactions + redux-tree and I'm pretty happy w/ it so far.

@thejmazz
Copy link

Approach in our current project:

note, would be very different depending on application requirements :) once we add in realtime object updates, perhaps sagas or observables would provide benefit over thunk/promise

  • redux-thunk
  • promise middleware
  • our "api wrapper", which is a wrapper around client feathers app, service(name, method, ...opts) which is an action creator with payload being a promise to call our API (i.e. promise middleware picks it up and dispatches x_FULFILLED, x_PENDING, and x_REJECTED). could also overwrite name of action for this.

We first use componentDidMount() to call our API, store data in component state. However, we use our api wrapper for this, which means an action is still sent out (and logged by logger middleware, including meta which is request info), and if we so desire to refactor into using the store for that component all we need to is add a reducer which listens on the action. We start by using local state only, and refactor into redux when that component's state needs to be accessed/modified from another component. That being said, I see the attraction of using redux everywhere, as it provides a paper trail for everything. Given our current team and application timeline, it's just not beneficial for us atm.

I've played with redux-saga a bit, but since our most complicated async flow is login/logout, which works (and code is not terribly complicated), not much of a huge reason to switch (but might be worth it for testing reasons - iterable + mocks is a nice way to test). redux-observable I don't see the immediate benefit unless observables would themselves provide benefit, for example, if you want to double-click events nicely.

We've gotten a bit away from boilerplate by having our own factory functions to return reducers, and having our own higher order reducers on top of custom functionality (e.g. a reducer on top of "paginator" so that content is paginated but can have custom actions to modify an item).

What I think needs to be done is a giant tutorial working up from a basic react app, demonstrate issues that come up with inter-component communication, then introduce redux, then go onwards, demonstrating problems that occur under specific situations, and how they can be helped with redux-x. That is, a guide for when and what to use. There are definitely some blog posts that exist out there with discussion in this direction.

What's also relevant is my summary I came up with for patterns used in gothinkster/react-redux-realworld-example-app

@jasonrhodes
Copy link

Most people I know who use redux heavily, end up moving away from redux-thunk as they find it doesn't scale very well

I understand what you're saying, but here's my flip-side concern: we're seeing more and more anecdotal evidence that a huge group of JS devs aren't even using arrow functions and other baseline ES(6|2015) features yet, due to lack of understanding, intimidation, etc. Expecting folks who want to get started with Redux, and who could benefit from learning the patterns that Redux introduces, to first learn observables or generators is I think probably asking too much?

Redux-thunk is also slightly complex in the sense that redux middleware in general kind of bends your brain, but it's at least just functions/callbacks, which are easy to pick up if you are writing JS at all. I really like the idea of having a complete package of related tools to get started with, available via a single download, even if it's pitched as "learn-redux" instead of "best-practice-always-redux". Similar to how create-react-app needs tweaking as you learn all the things it set up for you, this could encourage your own tweaking, maybe show how to convert a simple redux-thunk setup to using sagas, etc. as its own form of "ejecting" ...

Just some thoughts.

@Phoenixmatrix
Copy link

I understand what you're saying, but here's my flip-side concern: we're seeing more and more anecdotal evidence that a huge group of JS devs aren't even using arrow functions and other baseline ES(6|2015) features yet, due to lack of understanding, intimidation, etc. Expecting folks who want to get started with Redux, and who could benefit from learning the patterns that Redux introduces, to first learn observables or generators is I think probably asking too much?

Bingo! We're in an environment where a LOT of people are new to JS (or they "know" JS, but it's not their specialty, and they start from the deep end). Especially if those folks are experienced software engineers from another ecosystem (java, rails, etc), they will quickly try and apply the concepts they know before learning the ones they don't, and it won't quite work and they get stuck. I don't know what's the best way to convince folks to get a deep understanding of JS before jumping in a functional UX design pattern though.

@sunjay
Copy link

sunjay commented Mar 19, 2017

Note that the Redux docs do have a section about Reducing Boilerplate for those of you reading who are looking for something now and don't want to adopt an entire library on top of Redux.

We should be a little careful about this discussion and realize that this has probably been bikeshedded a lot already. The Redux team has heard, considered and rejected a lot of things that will probably get proposed here. It's quite possible that nothing will come out of this discussion if we only fight about things that have already been discussed.

That being said, I think it's a great idea to talk about ways to make the framework more accessible to everyone (new and experienced).

Anything you propose to reduce boilerplate should be such that it is possible to go back to the stuff below the abstraction whenever needed. It is not fun to adopt an abstraction just to drop it later to go back to the lower level stuff because you needed one extra thing that the authors of the abstraction didn't think of.

Redux learning curve can be steep, but once you grasp concepts, the need for
abstraction layers often goes away

I'm wondering, if this is the case, what parts of Redux are we suggesting to improve? What parts would you remove or abstract over that you wouldn't need to immediately add back in?

This is a diagram of the entire react/redux lifecycle I made for a talk at work about a year ago:
React Redux Lifecycle

There are quite a few parts of this, but I can't imagine Redux working as well without any one of them. Containers are kind of annoying at times, but if you remove them, you deeply couple your view logic with your data layout. If you remove actions, you're basically MVC again which is missing the point of using Redux in the first place. The "view" in that diagram already barely exists because it can be modeled as just a function that subscribes to the store and renders react components.

I don't think there is a lot of boilerplate to remove in the overall framework itself. Maybe you're referring to boilerplate in the individual parts of the framework like in action creators, reducers, or containers. In that case, the tips from the Reducing Boilerplate page mentioned above address most of those things already. We don't need anything on top of that to make it any "better". (Note: It's not that I'm not open to adding something to make things better, I just don't see it yet.)

Maybe this isn't so much a reducing boilerplate problem but a problem of improving Redux education. If the framework is hard to grok for beginners, we may need to improve the Redux docs and make it more accessible. Maybe some of the reducing boilerplate tips need to be advertised more aggressively in the docs.

Reducing the amount of steps required (boilerplate) does not always solve the problem. To stress my point from the very beginning of my post, you don't want to write an abstraction that will get thrown away because you didn't think of every way people would need to use it.

@markerikson
Copy link
Contributor Author

Some good discussion so far. Lemme toss out a few quick examples of common "boilerplate"-related complaints that I see:

  • "Why do I have to write 'action creator' functions just to return an object immediately?"
  • "I have to touch SOOO many files just to add a simple new feature! Also, I keep repeating the same names over and over for constants and function names and..."
  • "Why do I need these 'middleware' things just to make an AJAX call?"
  • "Why do I have to use switch statements for everything?"
  • "Why do I need this dispatch thing? Why do I have to wrap up all these functions to make them work?"

And some specific examples of these types of comments:

@markerikson
Copy link
Contributor Author

markerikson commented Mar 19, 2017

And to immediately toss out some responses to those "boilerplate" concerns: of those five categories listed, only "use of dispatch" is actually required. For the rest:

  • You don't have to use action creators, but it's a good practice for consistency (per my post Idiomatic Redux: Why use action creators? )
  • You don't have to have completely separate files for action creators, action constants, and reducers. The "ducks" pattern is a popular approach for putting all of those in one file together. That does have the downside of "hiding" the ability to have multiple reducers listen to one action. Redux ultimately doesn't care what your file structure is.
  • You can do async work outside of Redux completely, like in a component. But, per Dan's description at Why do we need middleware for async flow in Redux?, it's usually good to extract that logic outside of components, and middleware provide a "loophole" for doing async work while having access to the Redux store.
  • You definitely don't have to use switch statements in reducers, they're just the most "obvious" way of handling multiple values for a given variable. Lookup tables, if/else statements, and anything else you want are just fine (per FAQ: Do I have to use switch statements? )
  • You do need to actually call store.dispatch() to make anything useful happen - that's a basic design decision of Redux. Binding up action creators makes it possible to pass them around to unconnected child components and still have them dispatch whenever the function is called.

So overall, there's almost nothing out of these "boilerplate" concerns that's required. It's a combination of examples from the docs and "good programming practices" like de-duplicating code and separation of concerns.

@sunjay
Copy link

sunjay commented Mar 19, 2017

I think the questions brought up by @markerikson are really fair and I have asked them myself at some point in the past as well.

Redux is in a sense a "low-level" library for data modelling. Like any such low-level library, it exposes a lot of things that you could easily abstract over in order to account for the majority of cases. I think the reason @gaearon didn't originally do that is because he wanted to keep the library as small and flexible as possible. Thanks to that decision, we're able to build a lot of different things on top of Redux without needing to have everything in the Redux core.

Maybe we should consider that the right path might be to develop and stabilize a good library on top of Redux (like Jumpstate?). We start by teaching people that, and then give them an easy path to use Redux directly when they need to.

I don't think Redux needs very much more in its core codebase and I don't see any part of it that needs to be abstracted away permanently. (If you do, let me know 😄) In my opinion, there isn't a lot to gain by adding or removing things from Redux core.

Improving a library to the point where it's stable and flexible enough for everyone to use is probably a better option. Like you said, not much of the "boilerplate" is actually required, so let's get rid of it in a library on top of Redux instead of modifying Redux itself.

An example of this happening in another community is in the Rust programming language. There is a non-blocking IO library for the Rust programming language called "mio". It focuses on being small and low-level just like Redux does. The thing is, pretty much no one uses it directly because that would be really hard and full of boilerplate. Most everyone uses another library called tokio which builds on mio and makes it extremely usable and ergonomic. This way, anyone who needs the plumbing from mio can use that directly, but anyone who just wants to make something quickly can use tokio.

We should adopt a similar model.

@markerikson
Copy link
Contributor Author

markerikson commented Mar 19, 2017

To expand on a couple of @sunjay 's comments:

There was a recent comment in #775 that I think captures things well:

Redux is a generic framework that provides a balance of just enough structure and just enough flexibility. As such, it provides a platform for developers to build customized state management for their use-cases, while being able to reuse things like the graphical debugger or middleware.

So yes, Redux is "just a pattern" in a lot of ways. The core library really is feature-complete - the only real semi-planned changes are things like the proposed enhancer restructuring ( #1702, #2214 ), and possibly making combineReducers more flexible ( #1768 , #1792 , etc).

Almost two years have passed since Redux was created, and we now have a pretty good idea how people are using it. As one example, @jimbolla collected a list of all known store enhancers in #2214 , and categorized how they work and what they're used for. I'm still absolutely a huge fan of Redux's simplicity and flexibility, but I'd love to see some still-idiomatic abstractions on top of Redux that would simplify general usage and solve problems for people.

One other set of semi-related topics, and something that I have a distinct personal interest in, are the ideas of "encapsulated logic/components" (per @slorber 's "scalable frontend with Elm/Redux" playground ), and "plug-and-play Redux setup" (as seen in experiments like brianneisler/duxtape#1 , jcoreio/redux-features#7 , etc). Global state and app setup is great for some use cases, but not so much for others.

As an interesting related point, @toranb has done some great work creating an Ember wrapper for Redux at https://github.com/ember-redux/ember-redux . The Ember world is really into "convention over configuration", and so ember-redux sets up a reasonable default store config, including redux-thunk and such. Some kind of approach like that might be helpful.

So yes, I'm sorta throwing out a whole bunch of different thoughts here, but they are related in various ways. Overall, I want to lower the barriers for learning and using Redux, and enable more advanced use cases across the board.

@ntucker
Copy link

ntucker commented Mar 19, 2017

Redux is a general API, not specialized. This makes it more verbose, while also covering more cases. Its power is in complex software, whereas any new or simple project this is beyond the scope of necessity.

However you can build specializations on top of general APIs. For instance, I use https://github.com/acdlite/redux-actions which reduces boilerplate for common cases. I still use the full power of redux and simply having that interface would not be enough for my needs.

The other problem is with new people who haven't experienced the pain of incredibly complicated applications wondering what's the point. Well, to them they probably shouldn't even use redux until they have experienced that pain, but Dan's egghead series can kick them past this point pretty easily.

https://egghead.io/courses/getting-started-with-redux
https://egghead.io/series/building-react-applications-with-idiomatic-redux

Reference: The spectrum of abstraction: https://www.youtube.com/watch?v=mVVNJKv9esE

@Andarist
Copy link
Contributor

As mentioned - redux-actions is quite good at removing some boilerplate, removing constants out of equation by attaching .toString is a really neat idea I like, however I prefer to write this logic myself than to use redux-actions myself, but thats personal choice for keeping my code smaller.

Imho there should be a built-in mechanism (subscribe's callback argument?) for observing dispatched actions stream (i know it can be done in a middleware, but thats quite restricting use case, as it cannot be used outside, i.e. by components or connect)

@timjacobi
Copy link

I often times teach redux to others and it can be quite daunting for the following reasons

  • Many moving parts: store, state, reducers, action creators, actions, async actions, middleware, connected components
  • Libraries. For a simple app that talks to a REST API you need: redux, react-redux, redux-thunk (or others) + additional libraries if you want to test things
  • Amount of places you have to put code in (see moving parts)

I think redux is a great library that makes total sense in itself and I think it has great abstractions and is super scalable when used correctly. I also appreciate that redux can be used with any UI library but think that in the majority of cases it will be used with react.

Generally I believe a lot of frustration comes from the many moving parts.

Here are some thoughts that I had. All of which can be built on top of redux so it can stay as it is.

  • Provide a library like react redux which additionally comes with support for async actions built in.
  • A lot of async actions are talking to a REST API so provide an idiomatic middleware for that
  • Write smart components as actual components instead of using connect. They can then render dumb components in their render method
  • Prescribe a scalable file layout for container components, smart components, dumb components, action creators and reducers.
  • Provide a redux-cli to generate code e.g. redux action loadUsers
  • Standardise on an async layer like redux-saga or redux-observable and include it in the docs (It would be nice to have one place to consult for most redux matters)
  • default to extended language features (e.g. decorators (@connect)) or TypeScript to make things less verbose in particular mapStateToProps and mapDispatchToProps (mobx requires decorators and syntax is straightforward)

T

@markerikson
Copy link
Contributor Author

Agreed, redux-actions is a good example of some simple abstractions that remove boilerplate.

Let me ask this: Create-React-App has the react-scripts package, which has prebuilt Babel and Webpack configs, and comes with a bunch of sensible defaults built-in. What might a corresponding redux-scripts package look like?

@bodhi
Copy link
Contributor

bodhi commented Mar 19, 2017

I agree with some of the other (negative) comments about redux-thunk. I wouldn't recommend it to beginners (I probably wouldn't recommend it at all anymore):

  1. It seems like magic to start.
  2. It doesn't really take away that much boiler-plate. Given a more complex async process, having to get dispatch from props and pass it in works out to be only a small part of the code.
  3. It results in action creators that you cannot compose together into larger workflows.
  4. A last point that I can't articulate very well about it making the redux style into more of a framework than a library: you rely on the framework to call your code, rather than you calling into the library.

We used it pretty heavily in a React-Native application, and it was mostly useful, but we had trouble composing actions together (think "load profile after successful login"), due to redux-thunk action creators effectively returning void (ie. having nothing such as a promise to use for sequencing the next action).

It tends to encourage people to think that "the only way to do anything is to dispatch immediately from the React event handler/callback". And since we were using connect from react-redux and leaning heavily on mapDispatchToProps, we tended to forget that our event handlers could just be plain functions.

(note that the application above was mostly written early last year, and we didn't really keep up with what was happening in the greater Redux ecosystem, so it may be out of date).

@markerikson
Copy link
Contributor Author

@bodhi : without going off on too much of a tangent, I'd disagree with some of those conclusions. redux-thunk is 11 lines long, and there's some nice explanations out there like "What is a Thunk?". Thunks are composable to some extent, and I think it's generally easier to tell someone "write these couple lines of function declarations, then make your AJAX calls and such here". I have a post that discusses some of the various tradeoffs of thunks (and to some extent sagas) at Idiomatic Redux: Thoughts on Thunks, Sagas, Abstraction, and Reusability.

I do agree that there would need to be some "blessed" or "built-in" way of handling side effects in some hypothetical abstraction layer. Sagas and observables are both great, if you understand how to use them. I just don't know if they'd be appropriate, given that a potential goal here is to provide an ease-in path to learning and using Redux.

@dfbaskin
Copy link

I find that using redux-thunk is not just about doing something asynchronously, but also doing something that depends on the current state. Very clean, simple interface, and encourages the use of action creators.

@einarq
Copy link

einarq commented Mar 19, 2017 via email

@bodhi
Copy link
Contributor

bodhi commented Mar 19, 2017

(sorry to continue the tangent, but...)

it's worth mentioning that dispatch doesn't return void, it returns the result of your inner function.

Huh, great! Guess I just never looked too closely at the documentation before trying to find another way to solve our problems.

@aikoven
Copy link
Collaborator

aikoven commented Mar 20, 2017

The one most valuable thing that made Redux so much easier for me is treating actions as events and not commands. This completely eliminates the struggle to decide how to design actions and what to put in them.

Components don't have to know how user interaction should affect the state, they only tell the outside world what did happen inside them, e.g. "button X clicked". When using actions as commands, you effectively introduce a two-way binding between component and state.

Reducers are not bound to components, they represent application state and not component state, and they are the only party that actually knows how the state changes. There's no cognitive barrier in handling single action in multiple reducers because an action is not a statement "what to do with the state".

This also removes any case for dispatching multiple actions synchronously.

Any case for dispatching single action from multiple places also goes away. You can keep action creators near the component which makes it easy to track down the component that dispatched action.

By looking at action log in DevTools you can see a complete history of what happened inside your app and it's easy to see the concrete state slice affected by the action. The history remains the shortest possible.

This pull-based approach makes the data flow truly one-way and stacks naturally with side-effect management libraries like redux-saga or redux-observable.

@blocka
Copy link

blocka commented Mar 20, 2017

@aikoven although I totally agree with this (there was definitely a lot of discussion in the past comparing redux to event sourcing), what do you do with things like FETCH_USER, which is very common to see (and something that needs to be done). That seems more "command-like" then "event-like".

@aikoven
Copy link
Collaborator

aikoven commented Mar 20, 2017

@blocka There's always some event that results in a network request. The request itself can be wrapped in FETCH_USER_STARTED and FETCH_USER_DONE / FETCH_USER_FAILED actions, which is how request result ends up in the state.

@subodhpareek18
Copy link

subodhpareek18 commented Mar 20, 2017

👍 for redux-actions

What I also like about it apart from just reducing boilerplate is, it just feels like a very natural extension of redux. For something like jumpstate I feel the implementation is going a bit farther away, and now I'll have to kind of learn two things instead of one.

So my point is, any plugins we add to reduce boiler plate should feel very much like working with redux albeit one with a less verbose API.

@blocka
Copy link

blocka commented Mar 20, 2017

A lot of the boilerplate is the same boilerplate that you have in any environment. When you come across duplication, we usually solve it with a custom abstraction. These abstractions are, very often, not useful outside of your current project, and a "silver bullet" just can't exist. Every project's requirements are different.

Same thing with redux. If you fine that you need a FETCH_X_STARTED action, and a fetchX() action creator, and reducer to handle that, etc., feel free to make your own abstraction up!

Blaiming redux for boilerplate is like copying and pasting all your code, and then blaming javascript for having too much boilerplate.

@damaon
Copy link

damaon commented Mar 20, 2017

I find that using redux-thunk is not just about doing something asynchronously, but also doing something that depends on the current state. Very clean, simple interface, and encourages the use of action creators.

Using redux-thunk for synchronous code isn't a great idea and should be mentioned in docs.
If you compose many functions together you aren't certain that dispatch flow is not order dependent so it's harder to reason and test.

It seems better to me to have pure actions creators and simply use actions batching for more complex state modifications. This way you have only pure functions to test.

I recommend this blog post to get better understanding: http://blog.isquaredsoftware.com/2017/01/idiomatic-redux-thoughts-on-thunks-sagas-abstraction-and-reusability/

@dfbaskin
Copy link

dfbaskin commented Mar 20, 2017

@Machiaweliczny thanks for the article. I guess the question to me is more about where you end up putting your business logic (as discussed in https://medium.com/@jeffbski/where-do-i-put-my-business-logic-in-a-react-redux-application-9253ef91ce1#.3ce3hk7y0). You are not saying that your dispatch flow (thunk, epic, saga, whatever), should never have access to the current application state, are you?

@Phoenixmatrix
Copy link

Phoenixmatrix commented Mar 20, 2017

@dfbaskin

Keep in mind that Redux isn't domain driven development. "Business logic" is a much fuzzy concern here, and it's more "what concept's function signature best fit the logic I'm trying to implement". Your "business logic" is spread out across selectors, reducers, action creators and "orchestrators" (do we have a better word for this yet ?) depending on what you need.

The reducer, being (state, action) => state is where "logic that depends on state" generally should be. And sure enough, in things like the Elm Architecture, all such logic is in the "update" function (the equivalent to our reducer).

Unfortunately in Redux, the reducer can only ever handle synchronous logic. So asynchronous "business logic" that depends on state either needs to get it from the component via dispatch (if the component has that info, which is not always), or through your orchestrator concept of your choice (thunk, sagas, epic), which have access to state for that.

If it wasn't for the reducer's limitation, there would be no need for it. You actually see it when people use redux-saga or redux-observable: their actions/action creators usually become near-trivial (in most cases, plus or minus some normalization or formatting), and the saga/epic are almost "alternate reducers", on that you now have another thing that has access to action and state and return a "thing", and the actual reducer's complexity is diminished as a result. Reducers and orchestrators are very closely related (sometimes too closely, and it's not great to have 2 constructs that are interchangeable half of the time...but it's what we have, might as well enjoy it)

@modernserf
Copy link

Its worth differentiating between two different sets of ideas going on in this thread:

  • a "full-service" redux-based framework
  • additional features in redux core

I agree with @markerikson that redux core should not become a framework in itself, and I understand why one would want to keep redux-thunk and react-redux in separate packages. However, I believe there is both room and precedent for new "helper functions" in redux core.

Redux has a fairly small API surface area: createStore, applyMiddleware, combineReducers, bindActionCreators, and compose. But most of these are convenience functions. Only createStore is strictly necessary for Redux to work. Why do any of these belong in core?

  • applyMiddleware creates a simplified interface to store enhancers. But more importantly, applyMiddleware supports the middleware interface as a standard way of adding effects to redux stores. The middleware ecosystem is possible because applyMiddleware has a comparatively simple interface and allows users to run many middleware at once.

  • combineReducers provides a convenient form of reducer composition. It's far from the only way to compose reducers, but it's certainly one of the more useful patterns. But its presence in redux core means that it's not just common but ubiquitous. Every nontrivial Redux app that I have encountered uses combineReducers to create the root reducer. The flipside of this is that because combineReducers is the only form of reducer composition that comes with Redux, many users seem to believe its the only form of reducer composition that exists.

  • bindActionCreators reduces some of the boilerplate around dispatching actions. But perversely, bindActionCreators is also indirectly responsible for some of Redux's reputation for boilerplate. This is because the mere existence of bindActionCreators encourages the use of action creators. This is particularly visible when one compares the prevalence of action creators (which are "endorsed" by bindActionCreators) versus selectors (which are documented but have no equivalent core function). I've seen many codebases using react-redux where mapStateToProps is always "bespoke" selectors but mapDispatchToProps is always an object map of prewritten action creators.

None of these are essential to the functionality of Redux yet all of them are essential to its usability. None of these functions limit the inherent flexibility of Redux, but by reifying common patterns into callable functions they make choices less daunting. And each of these helper functions raises further questions:

  • If middleware is a simplification of store enhancers, are there other simplifications that could be widely useful?
  • What other forms of reducer composition could be implemented instead of merely documented? Reducer objects? Dependent reducers? Linear composition?
  • Would integrating reselect lead people to create and compose selectors where they hadn't before? Would this be a net positive?

Even if the answers to all of these are negative, I think we do ourselves and the community a disservice by refusing to even consider their premises.

@markerikson
Copy link
Contributor Author

@modernserf : First, thanks for your comments, as well as your other recent thoughts on Redux. They are all extremely valuable and informative, and should probably be required reading for anyone else in this thread. So, I'll link them here:

It's very interesting that you should bring those up, because of the history involved (which I just spent a lot of time researching for my post The Tao of Redux, Part 1 - Implementation and Intent. In particular, redux-thunk was originally built right into Redux, and react-redux was only separated out later as well.

You're right that those utilities do indeed codify certain usage patterns. That said, those utilities are included because they codify the intended usage patterns: composition of slice reducers, a pipeline of middleware for async behavior and other centralized behavior, and bound action creators to simplify the process of dispatching actions from components (particularly passing functions around to child components). Somewhat similarly, while Reselect isn't in the core, Dan did explicitly encourage its creation.

So yes, anyone "could" have written those, but by building them in, we've pushed people to use those specific tools in specific ways. Definitely agree on that.

Per your last three questions:

  • I guess you could say that middleware are "a simplification of store enhancers", although I don't think that does them justice. Rather, it's a store enhancer for a very specialized purpose. As far as other store enhancer-related work: Dan has said that the one major part of the Redux core he would still like to modify relates to how enhancers are initialized. There's actually two "competing" PRs to change store enhancer implementation that have been sitting open for a very long time: @acdlite 's version in Make createStoreBase less opinionated by moving checks into coreEnhancer #1706, and @jimbolla 's version in Alternate Proof of Concept: Enhancer Overhaul #2214. It's also interesting to note that Jim put together a list of all known store enhancers to help analyze how the community is actually using them. I know that Jim's proposal was intended to help enhancer authors simplify commonly seen behaviors. So, that's a direction that could be explored further.
  • There's definitely plenty of other patterns for reducer composition and structure out there. I'm not sure exactly what we could do as far as "dependent" reducers, since that's much more niche use case and would seem to be pretty app-specific. On a related note, for a long time, Dan had closed any PRs that proposed adding some kind of third arg to combineReducers (and there's been a lot of them), but finally allowed Feature Request: Allow reducers to consult global state #1768 to go forward for a while. That PR has also stalled out.
  • The most common use case for selectors seems to be directly extracting bits of state and passing them as named props. We've also got an open PR to React-Redux for adding an object shorthand syntax for mapState, and have had numerous requests for that even though you can do the same thing with Reselect's createStructuredSelector.

As I documented in that "Tao of Redux" post, the stated goals were to keep the Redux core API as minimal as possible, and encourage an ecosystem on top of it. In addition, early in Redux's development, Andrew made a comment about the intent to "bless" certain plugins:

As gaearon 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.

Earlier up-thread, I had proposed some kind of "easy Redux setup" abstraction lib, or something like that. Would something like that, or at least that list of "blessed" addons, be sufficient along the lines that you're thinking? (And, actually, you did too in the first comment of this thread.) If not, any additional proposals or ideas?

@modernserf
Copy link

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.

This is what I would do as well, though I'd probably take it a step further: I would restructure the project into a monorepo, where the current redux package becomes something like @redux/core, blessed libraries like reselect and redux-action are brought into the repo and namespace, and the redux package itself just reexports all of these packages into a single namespace.

That sort of satisfies the constraint of not modifying redux core. Also, the extended version would be a strict superset of -- and therefore backwards compatible with -- the core. The newly features would be purely opt-in and have no special access to private state. As far as "bloat" goes -- each of these libraries is small to begin with, and they're written in such a way that it would be trivial to tree-shake them.

We've also got an open PR to React-Redux for adding an object shorthand syntax for mapState, and have had numerous requests for that even though you can do the same thing with Reselect's createStructuredSelector.

I think this should tell you something.

The monorepo structure I'm suggesting is basically the structure used by Lodash. Now -- Lodash is an interesting library to model after: its an enormous collection of functions, many of which are trivial to implement yourself. Nearly every function in lodash could be more flexible as a "recipe" -- groupBy is really a pattern after all; who are we to say what data structures you use it on?

And every lodash function is also available in its own a la carte library. There's no functional benefit to import { groupBy } from "lodash" over import groupBy from "group-by". Why should I have to deal with tree-shaking when I can just install what I need?

But the experience of using Lodash is fundamentally different than using recipes or nanolibraries. You know that if you're doing something with an array and you get a feeling like you're repeating yourself, there's probably a Lodash function to do it. You don't need to look up a recipe, you don't need to install anything -- chances are you don't even need to add another import line to the file. Its removing only the tiniest amount of friction but our days are filled with moments like this and it adds up.

@ajhyndman
Copy link

I hope you don't mind me adding my own 2 cents, from a different perspective.

The CQRS, Event Sourcing, and ultimately Domain Driven Design schools of thought are the greater ancestors which bore us Flux and Redux. While these influences are occasionally cited, Redux and its descendant tooling took most of their API terminology from Flux and Flux implementations which were popular at the time. This is fine, but I think we're missing some opportunity to profit from existing exposure developers may have had to those ideas.

To give two examples of the difference: In most Event Sourcing discussion I've seen, Redux Actions would be referred to as Events. Redux ActionCreators would be referred to as Commands. I think there's some added clarity and semantic that comes for free with this terminology too. Events are loggable, serializable data. Events describe what happened in the past tense. Events can be replayed. Events cannot be un-recorded. Commands have an imperative mood. Commands trigger Events.

I suspect that Event Sourcing as an architectural patterns will gain in popularity over the next decade, and I think that Redux could gain some usability, clarity and ease of use wins by aligning the API and discussion more closely with the broader community.

@blocka
Copy link

blocka commented Jul 18, 2017

I think this discussion was already beaten to death: #351

@ajhyndman
Copy link

ajhyndman commented Jul 18, 2017

Oh, I wasn't aware of that, thanks! 👍

It looks like #351 was closed because at the time there was no action that could be taken. If we are at a juncture where we are reconsidering API decisions and the language we use, this seems like an appropriate time to resurface the idea, to me.

I can be more concrete.

Here's my idea of what an API inspired by Event Sourcing idioms might look like:

Concepts

  • Observable Event Stream
  • Event Stream has a well-typed set of supported Events
  • Commands dispatch to the Event Stream
  • One or more Clients consumes the Event Stream

API

import { createEventStream, createProjection } from 'redux';

// Initialize the event stream separately from the store.  This becomes the one
// true source of truth for your application.
const eventStream = createEventStream({
  // Commands are the only thing that we want to couple to the eventStream.  The 
  // set of events which may end up in an eventStream should be easy to predict.
  //
  // A definition like this supports static analysis inference well for 
  // consumers that can leverage it.
  increment: () => ({ type: 'INCREMENT' }),
  decrement: () => ({ type: 'DECREMENT' }),
});

// Multiple stores with disjoint or overlapping data can be used to consume the 
// same event stream.
const store = createProjection(eventStream, reducer, init);
const adminStore = createProjection(eventStream, adminReducer, init);

// We don't need a jargon term ("Middleware"), or a dedicated hook to handle 
// async anymore.  We just register more subscribers to the eventStream.
eventStream.subscribe(myCustomMiddleWare);
eventStream.subscribe(sendEventsToAnalytics);
eventStream.subscribe(logEventsForPlayback);

// Calls to commands can be wrapped with React Providers or container components 
// in the same way that Redux currently does.  They can also be called directly.
eventStream.increment();

@markerikson
Copy link
Contributor Author

@ajhyndman : while I do appreciate the suggestion and discussion, that bikeshed has totally been painted and the horse has fled the barn (to completely mix metaphors). Dan opted to use Flux terminology rather than CQRS/ES terms, and there are no plans to change the core APIs or the terms used for Redux concepts.

(Also, while I have zero stats to substantiate this, I would guess that at this point there are more people familiar with Redux's use of "actions" and "action creators" than with CQRS/ES terminology, at least within the JS community.)

@ajhyndman
Copy link

I definitely get the impression that this conversation has been had before, and there's a strong desire not to re-open it. 😄 I won't press too much harder here. (I'd be interested in reading or continuing this conversation elsewhere, if there's a better touchpoint.)

Of course, you're also right that there is an entrenchment factor, and changing the entire API surface and terminology would be costly at this point in time.

I would still argue there's opportunity for the Event Sourcing and Redux communities to learn from each other.

  • What possible abstractions could be created that simplify the process of learning and usage, but without actually hiding Redux (and would hopefully provide a migration/learning path to "base" Redux)?

Even without borrowing the terminology or changing the API, I think there are wins we can find. For instance, I have had success introducing people to Redux by describing a store as "a state container in which currentState is a "pure function of" (it is derived from, or reduced from) an append-only list (or a stream) of Actions".

Looking further into the future, I think it's entirely possible to implement and abstract a redux-like server infrastructure (see: Apache Samza, and some of the other work from LinkedIn's engineering team). If we aspire to that, then it should also be possible to use similar abstractions for client and server state management! If we can achieve that, why not an isomorphic, persistent Redux store?

Any concepts that the JavaScript and Infrastructure communities are able to come together on, or easily map between, the more quickly I expect these opportunities to become apparent and the more expressive the abstractions.

I guess I'm just getting a little excited about some of these ideas! I hope you'll forgive me for the word dump.

Side note: It seems to be possible to create an event-stream-like wrapper API for Redux that implements the surface I suggested above!

https://github.com/ajhyndman/redux-event-stream

@Koleok
Copy link

Koleok commented Jul 19, 2017

@ajhyndman I think the wrapper is the right way to go with the your idea there 👍

Related to the samza and linkedin engineering work you mentioned, If others have not read/watched the wonderful talk Turning the database inside-out with Apache Samza then please find an hour to do so sometime! I think I saw dan mention it in a readme or tweet at some point, This talk changed how see databases and state management forever and is a very close idealogical cousin of redux.

@ajhyndman
Copy link

That video is awesome! It's also actually the second item in the list of Thanks on the redux repo's README.

@mariusandra
Copy link

Hey everyone!

Without being aware of the existence of this thread, I have developed a library that's in a way a direct answer to @markerikson's original questions and knocks off most things from @gaearon's list.

The library is called Kea:

screen shot 2017-08-06 at 13 10 03

It is an abstraction over redux, redux-saga and reselect. Except for the glue connecting them together, Kea doesn't pretend to create anything new and exposes raw redux action creators and reducers, raw redux-saga sagas and raw reselect selectors as needed. It doesn't invent new concept.

Kea supports many different modes of operation: You can use it standalone, connected to React components, inlined above React components or even dynamically connected to React components where the input to the selectors depends on the props of the React component.

I personally use it with two large applications: one marketplace for private lessons and in one huge fleet tracking software. Plus I know of people who have built their own apps with it. Because it was so useful for us, I spent considerable time cleaning it up and writing documentation.

For me this library has always greatly reduced boilerplate, while remaining true to the core of redux itself.

Anyway, enough talk, here's some code. This is an example of what I call "inline kea" - where the logic is attached to your component directly. This mode is great for when you're just getting started or as an alternative to React's setState.

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { kea } from 'kea'

@kea({
  actions: () => ({
    increment: (amount) => ({ amount }),
    decrement: (amount) => ({ amount })
  }),

  reducers: ({ actions }) => ({
    counter: [0, PropTypes.number, {
      [actions.increment]: (state, payload) => state + payload.amount,
      [actions.decrement]: (state, payload) => state - payload.amount
    }]
  }),

  selectors: ({ selectors }) => ({
    doubleCounter: [
      () => [selectors.counter],
      (counter) => counter * 2,
      PropTypes.number
    ]
  })
})
export default class Counter extends Component {
  render () {
    const { counter, doubleCounter } = this.props
    const { increment, decrement } = this.actions

    return (
      <div className='kea-counter'>
        Count: {counter}<br />
        Doublecount: {doubleCounter}<br />
        <button onClick={() => increment(1)}>Increment</button>
        <button onClick={() => decrement(1)}>Decrement</button>
      </div>
    )
  }
}

In case your app grows and more places need access to the increment and decrement actions or the counter prop, you can just split your code like so:

// counter-logic.js
import PropTypes from 'prop-types'
import { kea } from 'kea'

export default kea({
  actions: () => ({
    increment: (amount) => ({ amount }),
    decrement: (amount) => ({ amount })
  }),

  reducers: ({ actions }) => ({
    counter: [0, PropTypes.number, {
      [actions.increment]: (state, payload) => state + payload.amount,
      [actions.decrement]: (state, payload) => state - payload.amount
    }]
  }),

  selectors: ({ selectors }) => ({
    doubleCounter: [
      () => [selectors.counter],
      (counter) => counter * 2,
      PropTypes.number
    ]
  })
})
// index.js
import React, { Component } from 'react'
import { connect } from 'kea'

import counterLogic from './counter-logic'

@connect({
  actions: [
    counterLogic, [
      'increment',
      'decrement'
    ]
  ],
  props: [
    counterLogic, [
      'counter',
      'doubleCounter'
    ]
  ]
})
export default class Counter extends Component {
  render () {
    const { counter, doubleCounter } = this.props
    const { increment, decrement } = this.actions

    return (
      <div className='kea-counter'>
        Count: {counter}<br />
        Doublecount: {doubleCounter}<br />
        <button onClick={() => increment(1)}>Increment</button>
        <button onClick={() => decrement(1)}>Decrement</button>
      </div>
    )
  }
}

Please try it out and read the docs to see more about side effects through redux-saga and other features you can use.

The feedback for Kea has been overwhelmingly positive until now, quoting from an issue: "More people should use KeaJS as its making redux world a better place! 👍" :)

Thank you for your time and for reading this far! :)

@davegri
Copy link

davegri commented Aug 6, 2017

Looks nice! not really liking the magic strings though 'increment'

@lucfranken
Copy link

Did this discussion move forward on another place? Of is Kea the most correct outcome which is generally accepted?

@markerikson
Copy link
Contributor Author

@lucfranken : no, the discussion fizzled out.

If you're looking for a higher-level abstraction over Redux, then Kea is probably a good option. If you're looking for a "Redux starter pack" that simplifies the store setup process, you may have to look around some. I know I've seen some things like that out in the wild, but we don't have any official lib like that right now.

@HenrikJoreteg
Copy link

Throwing my hat in the ring here. This is how I address this issue: https://github.com/HenrikJoreteg/redux-bundler

I personally think it's a big simplification and reduction of boilerplate.

It's how I've been building everything I build recently.

@anish000kumar
Copy link

Guess it would help :
https://github.com/anish000kumar/redux-box
image

@adrienharnay
Copy link

adrienharnay commented Feb 14, 2018

Hey, in my company we just open sourced a library which manages the network layer and removes a lot of boilerplate from Redux. It has proven successful for us, but try it out yourself: https://github.com/Brigad/redux-rest-easy/ (medium article: https://engineering.brigad.co/introducing-redux-rest-easy-6e9a91af4f59)

1_327nnvo3cuaqc-dsqpoq7w

@zmitry
Copy link

zmitry commented May 31, 2018

here is my approach to reducing redux boilerplate
image

https://github.com/zhDmitry/restate

@markerikson
Copy link
Contributor Author

As a highly belated necroing of the thread, a while back I asked for feedback on Twitter for what "boilerplate" means to people:

https://twitter.com/acemarke/status/969040835508604929

@markerikson
Copy link
Contributor Author

Return of the Necro-Thread!

Per my edit of the first comment in this thread, the ideas discussed here have been turned into our Redux-Starter-Kit package. Please try it out and see how much it can help simplify your Redux usage!

@markerikson
Copy link
Contributor Author

Revisiting this thread one last time.

We released Redux Toolkit 1.0 in late 2019.

Since then, we've made it the default way to write Redux logic, now teach it as the default in our new tutorials, and its usage has grown considerably - in fact, it just overtook Mobx in daily downloads within the last couple weeks.

Dan commented in this thread back in 2017 with his thoughts:

  • A Flow/TypeScript friendly subset that is easier to type than vanilla Redux.
  • Not emphasizing constants that much (just make using string literals safer).
  • Maintaining independence from React or other view libraries but making it easier to use existing bindings (e.g. providing mapStateToProps implementations).
  • Preserving Redux principles (serializable actions, time travel, and hot reloading should work, action log should make sense).
  • Supporting code splitting in a more straightforward way out of the box.
  • Encouraging colocation of reducers with selectors, and making them less awkward to write together (think reducer-selector bundles that are easy to write and compose).
  • Instead of colocating action creators with reducers, getting rid of action creators completely, and make many-to-many mapping of actions-to-reducers natural (rather than shy away from it like most libraries do).
  • Making sensible performance defaults so memoization via Reselect “just works” for common use cases without users writing that code.
  • Containing built-in helpers for indexing, normalization, collections, and optimistic updates.
  • Having built-in testable async flow support.

Today we've released Redux Toolkit 1.6 with the new "RTK Query" data caching API. While it doesn't normalize and deduplicate requests, it does fulfill the "built in helpers for normalization, collections, and optimistic updates" criteria.

It's safe to say that we've successfully achieved the goals laid out in this issue, and we encourage anyone who hasn't tried out RTK to check it out today!

@reduxjs reduxjs locked as resolved and limited conversation to collaborators Jun 7, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests