Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Redux is not over-engineered. It's a simple library that's <1000 lines of code. It is great for large, complex apps where state gets hairy to manage. It's a bit verbose for smaller apps. But just because something is verbose does not mean it's over-engineered. It is explicit in defining all the possible states your application can get in. That's useful for a certain class of application.


It might have been more accurate for me to have said "the code you will get if you follow the reference implementations and documentation of Redux will be overengineered". I dislike three things: that dispatch isn't usually made globally available, but is passed through context, which makes it effectively global anyway; that asynchronous actions are only available via plugins despite being a core part of JS development; and that dispatching an action is just a more complicated way of calling a function.

Interested in hearing what you think, though. My Redux experience has been more limited by trying to avoid it on small projects.


Hi, I'm a Redux maintainer, and I'd like to clarify a few things.

First, your points about `dispatch` and `context` are specifically about the React-Redux bindings, not the Redux core itself. React-Redux is specifically intended to act as an abstraction layer so that your own components are "unaware" of Redux, which keeps them more reusable and more testable. It also saves you from needing to write store subscription handling every time you want to make use of data from the store. My "Redux Fundamentals" workshop slides [0] show examples of what it would look like to hand-write store subscription code all throughout your UI, and it would be a pain.

If you _really_ want to, there's nothing stopping you from importing the store globally across your application and using it, but that misses out on the benefits of React-Redux, and also ties you to that one specific store instance.

Second, you can absolutely do async logic without any middleware. However, one of the key design points of Redux was to allow users to choose which approach they want to use to handle async logic, without limiting users to whatever was built in to the core. I discussed this in my "Redux Ecosystem" talk at ReactBoston last year [1].

You can certainly do async logic without any special middleware - just `connect()(MyComponent)`, throw in a `setTimeout`, and call `this.props.dispatch()`. The point of `redux-thunk`, the most common async middleware, is that it provides a generic way to move async logic outside of your UI and make it reusable [2].

Finally, the "dispatching an action === calling a function" comparison can be true, but only if you're using Redux with a limited mental approach and treating actions like "setters". If you start thinking about actions as more of an "event that occurred", then that leads to having multiple parts of your reducer logic independently respond to the same action and update their own pieces of state (which is an encouraged usage pattern). Justin Falcone had some good thoughts on this a while back [3], and I talked about this some in a fewmore of my blog posts [4] [5] [6].

[0] https://blog.isquaredsoftware.com/2018/06/redux-fundamentals...

[1] https://blog.isquaredsoftware.com/2017/09/presentation-might...

[2] https://blog.isquaredsoftware.com/presentations/workshops/re...

[3] https://medium.freecodecamp.org/whats-so-great-about-redux-a...

[4] https://blog.isquaredsoftware.com/2017/01/idiomatic-redux-th...

[5] https://blog.isquaredsoftware.com/2017/05/idiomatic-redux-ta...

[6] https://blog.isquaredsoftware.com/2017/01/practical-redux-pa...


This is all absolutely true! Redux and React-Redux, and their associated documentation make a lot of intelligent design tradeoffs that have a lot of thought put into them, and that's worthy of respect. "Overengineered" was too strong a term, and I shouldn't have used it. Sorry.

However, some of the things that have been traded are default configuration that represents typical use, standardisation across the ecosystem, and brevity. I think that for a lot of use cases, these tradeoffs make React-Redux harder to use, and that as a whole they make it easier to cause significant architectural problems for a newer or less technical user.

> If you _really_ want to, there's nothing stopping you from importing the store globally

What I've taken to doing is exporting a dispatch function that calls the store's dispatch, not the store. I think this better captures the intention behind Flux than a higher-order component, without much cost, since dynamically swapping between stores doesn't seem common.

> ...one of the key design points of Redux was to allow users to choose which approach they want to use to handle async logic...

This power and choice, while allowing a lot of freedom, comes at the cost of a fragmented ecosystem. The Redux documentation page for async actions lists six different packages that can help.

> the "dispatching an action === calling a function" comparison can be true, but only if you're using Redux with a limited mental approach and treating actions like "setters"

I may not have explained that very well. Actions themselves make perfect sense from the perspective of a reducer, and clearly they must be dispatched, but they're a state management detail that the UI doesn't need to know about. Something similar to redux-action could be the default.

I suppose my core problem with Redux/React-Redux is that I don't understand why a typical component in a typical app doesn't look like

  import { connect } from 'redux'
  import { deleteFoo } from '../actions'
  
  const FooList = ({ foos }) => {
    return foos.map(foo => <button onClick={() => deleteFoo(foo.id)}>Delete {foo.name}</button>)
  }
  
  export default connect(state => ({ foos: state.foos }))(FooList)


Yeah, for a while it seemed like there was a new Redux side effects lib coming out every week. At this point, though, it's pretty much standardized on thunks for basic use cases, and either sagas or observables for more advanced use cases based on your preference of writing async logic.

> What I've taken to doing is exporting a dispatch function that calls the store's dispatch, not the store.

Not sure I follow that train of thought - could you point to an example?

> Actions themselves make perfect sense from the perspective of a reducer, and clearly they must be dispatched, but they're a state management detail that the UI doesn't need to know about. Something similar to redux-action could be the default.

This is why we recommend use of action creators, so that a component is just calling `this.props.doSomething()` without it "knowing" that a Redux action is actually being dispatched.

Per your snippet there at the end:

- `redux` and `react-redux` are deliberately separate packages, because the Redux core is 100% vanilla JS, and independent from React (in the same way that `react` and `react-dom` are separate packages - the core React logic is independent of the platform-specific reconcilers). So, `import {connect} from "redux"` wouldn't be appropriate.

- The use of `deleteFoo()` that way assumes that it's pre-bound to a specific store instance, which limits the reusability of your logic, and also makes it a lot harder to test. `connect` abstracts away the question of "which store instance am I interacting with?", so that your components and logic are more testable and reusable.

> Something similar to redux-action could be the default.

For what it's worth, earlier this year I threw together a small `redux-starter-kit` package: https://github.com/markerikson/redux-starter-kit . The goal behind it is to simplify some of the most common pain points around Redux: store setup (including thunks and devtools), writing immutable updates in reducers, and having to install multiple packages out of the box. The biggest thing I know it's still missing at this point is indeed something akin to `redux-actions` - see my notes at https://github.com/markerikson/redux-starter-kit/issues/17 .

I haven't had time to push it forward further, but I would seriously like to get some more eyes on it, see if there's any other use cases we should be covering, and then make it an official Redux-branded package and update our docs to recommend that people use it.


> Not sure I follow that train of thought - could you point to an example?

Something like

  const store = createStore(...)
  export const dispatch = (action) => store.dispatch(action)
It removes the temptation to read directly from the store.

> `redux` and `react-redux` are deliberately separate packages

Yeah, my bad, got the import wrong.

> ...limits the reusability of your logic...

When would this become a problem, in practice? Reusable actions across different react/react-native UI, or multiple apps? Switching between user accounts? How common a use case is it?

Even if this is a constraint, I'm of two minds about whether a hack in isolation is worse than more complicated action boilerplate in general.

> ...and also makes it a lot harder to test...

This can be mitigated by Jest's mocks.

> https://github.com/markerikson/redux-starter-kit

That's really good. Unfortunately, it's not the same thing as bundling it with Redux proper.

Anyway, thanks for engaging with me. I never intended to do a deep dive into Redux's design, only to sympathise with stupidcar's comments about third-party state management, but it's interesting to hear the perspective of a maintainer.


Sure. I'll toss in a few more thoughts here in response.

Your `dispatch` snippet could be simplified to just `const {dispatch} = store`, because the store isn't a class instance - it's a closure. However, I'm not sure why you'd feel a need to "remove the temptation to read directly from the store". In what scenario are you trying to dispatch actions (presumably from the UI), but not read any data?

A basic example of reusability is the unit testing scenario itself. Ideally, you want to minimize the amount of other app code that's getting pulled in, so you can test this one piece in isolation. A React component shouldn't "know" about a store at all - it should just be getting data and functions as props. Similarly, even async logic _should_ be testable in reasonable isolation. If you are directly importing a singleton store instance throughout the entire app, you're coupling all of your logic to that one instance.

Typical testing of store-related logic involves the `redux-mock-store` package, which is just similar enough to a real store to let code run, but records which actions have been dispatched. Similarly, if you do want to test a Redux-connected component and its pieces all together, you would normally provide a unique store or mock store instance there in the unit test - you don't want to drag in the rest of your codebase to test that one piece. (And sure, Jest has some pretty good mocking abilities, but not everyone's using Jest.) I've got some slides on standard Redux unit testing practices [0], and a large section on testing in my React/Redux links list [1].

Outside of testing, yes, reuse of React+Redux across multiple platforms is viable, as is sharing code in libraries. But, again, that only works if the store is effectively dependency-injected at runtime, and that's the point of how things like `connect()`, thunks, and sagas are set up.

I agree that a separate "starter" package isn't the same as putting that all in the core, but that's kind of the point. The starter kit depends on several other packages (`redux-thunk`, `immer`, `selectorator`, etc), while the core Redux library is tiny and standalone. The intent has always been to keep the core small, and let people build pieces on top of it. So, for this package, the specific goal is to help simplify the most common use cases and concerns I've seen, while still avoiding dictating the rest of their app design.

[0] https://blog.isquaredsoftware.com/presentations/workshops/re...

[1] https://github.com/markerikson/react-redux-links/blob/master...




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: