Hacker News new | ask | show | jobs
by eropple 3032 days ago
I've never really understood the point of redux-thunk. But, then, I also don't use asynchronous actions in my datastore.

Asynchronous actions feel, to me, that they belong at the component layer where niceties such as spinners are being rendered, and then the backing store is updated with the results of the triggered action.

What drives people to put all of that into their store?

6 comments

Thunk is not about storing async state in the datastore, it's more akin to multithreading in my opinion.

In backend, you might spin up a thread to do expensive operation like API access, and that thread has it's own database connection, to read and persist data.

With thunk, you dispatch a thunk-ed action to do expensive operation like fetching data from backend, and that action has it's own access to redux in form of `dispatch` / `getState` arguments.

In the end, my component calls `fetchData` function upon mounting. And the logic dealing with making multiple requests due to paging, or re-trying on errors then lives its own life inside a thunk action, independent from the component that spawned it.

How do you test the code being run inside the Redux store? How do you mock your API client?
A thunk action `fetchData` would return a `(dispatch, getState) => {}` function. You call that with `getState` that would return your fixtures, and verify that `dispatch` gets called as expected. For API, I use jest and mock out imported functions.
Redux is for state management - that could be your data store but many people use it manage their app's UI state as well. Dan Abramov himself has said many times that redux isn't strictly necessary and using local state is fine, but there are some benefits to having a full, serializable application state. You can capture and include it with a bug report/runtime exception for example. Having redux actions dispatched when users interact with your app can also be useful since you get an implicit global event bus. For things like analytics instead of littering calls to your analytics provider in all of your components' click handlers you can write middleware and keep all of that code outside of your presentation/behavior layer.
> full, serializable application state

Perhaps I'm dense, but you can serialize the current running state of a Promise in redux-thunk?

One approach is to write a reducer which has `isWaiting`, `error`, `finished` bools (in addition to whatever data said promise eventually modifies), or something similar, and programmatically update them in your actions/thunks. This effectively tracks the state of the promise (resolved/not resolved, error).I think the Flux "standard action" puts something similar in the action object - not sure what they do with it then.
I always put a `pending` property on the `meta` object, and any component that needs to show network status can just check that property on every render cycle. Aside from requiring some extra utility functions in the reducers to prevent having to updating `pending` for every state of the request it's rock solid and I haven't ever encountered a situation where it wouldn't work. You can get a _lot_ of mileage out of conforming to the FSA standard[1]; network status goes in meta, and errors are always handled the same way.

[1] https://github.com/redux-utilities/flux-standard-action

Agreed. And that sounds like a great approach. We didn’t conform exactly to FSA (which I want to try out on future projects). Our isWaiting was equivalent to your pending, I think.

(Edit) FYI we had actions to set isWaiting, etc.

Also, this conversation got me thinking about the difference between application state, state of an asynchronous request, and Redux actions as events (I'm thinking of the request/response events in node.js), and using them as such - to pass data throughout the application (in addition to the payload). Which I guess is what FSA is all about? So, thank you!
Yeah I get that, but then the application state isn't serialized in a usable form. If you re-load that state, you have half-open, "pending" operations.

If an idea of Redux is that serializable state is a good thing, then redux-thunk seems inimical to that.

Why would you serialize a pending network request? I use a property whitelist in my serializer that drops any application state not related to the business logic. Also, I don't use redux-thunk, I move all async operations to middleware and action creators are synchronous and declarative. For example, an API request will be defined in an action creator as an endpoint, http method, and request body, and the middleware will intercept this, make the API calls, and dispatch "pending" and "success/failure" actions. It scales extremely well and I don't know why people don't use custom middleware more often. The currying is a little confusing if you've never been exposed to it before, but the concept is no different than middleware in pretty much any http framework. I have yet to run into a problem that can't be solved by a custom middleware chain more cleanly than crazy async action frameworks like saga (I know saga works great for some folks but I think the cognitive overhead makes it of questionable utility when a promise chain in middleware can solve the same problem without requiring everyone on your team learn yet another library).
> Why would you serialize a pending network request?

I wouldn't. That's why I wouldn't put anything related to it in my Redux state!

But, like, I'm willing to be convinced. This is just where I'm at right now. Maybe your method would make more sense than redux-thunk does to me; have you written anything in more detail about this? (I'm not scared of middlewares, I've used Redux and middlewares even in non-web contexts, so intuitively this at least sounds more promising.)

The problem with putting all async code directly into components is that often, async code is business logic or some other non-view-layer thing that doesn't really belong in a component.

The benefit of redux-thunk (and there are obviously other alternatives that provide the same value) is that it lets you write plain functions that have nothing to do with the store, and nothing to do with components. If you put all your business logic into these plain functions, then they're really easy to test, reason about, move, refactor, etc.

> What drives people to put all of that into their store?

It's worth pointing out that redux-thunk doesn't move async code into your store. The store (i.e. reducers) is still completely synchronous.

> async code is business logic or some other non-view-layer thing that doesn't really belong in a component.

In many React applications, components are separated into independent "controller"-layers and "view"-layers (containers and components are nomenclature I see occasionally, but the important takeaway is that both inherit from the React component class). Business logic has no place in your view components, of course, but is quite appropriate in your controller components. I don't think component implies view layer here. Even redux actions, when used, are appropriately called from the controller layer and not the view layer. Redux doesn't change anything about logical separation of concerns.

When the Flux pattern was originally introduced, it was driven home that it should not be used for everything. redux-thunk seems to encourage that you use it for everything. What do you use when global state is not necessary (or wanted)?

> Business logic has no place in your view components, of course, but is quite appropriate in your controller components.

Sure. Where you actually put stuff depends entirely on the complexity of what it is that you're building. If you have an async function that you need to use in several places then it makes sense to keep it separate. If you have several of these then maintaining a separate container component for each view component that needs access to these quickly becomes unmaintainable.

On the other hand, keeping them as separate functions and adding them in via redux `connect` syntax and `mapDispatchToProps` is simple and makes it clear what's going on. In this scenario, the `connect` higher-order-component acts like the container class, but it built up from separate parts depending on what you need.

> redux-thunk seems to encourage that you use it for everything

I don't think this is true. redux-thunk is a piece of middleware and that's pretty much it. It should be used however is most appropriate for your application and your particular use case. Keeping state inside components isn't always appropriate, but isn't always a bad thing either. If one is growing too complicated, then perhaps the other is more appropriate.

I agree that no two applications have the same needs, but async functions (and non-async, for that matter) are naturally separate and reusable, so I'm not sure I am entirely clear on what you are getting at here. What is the benefit of using connect to connect your function and simply calling that function from your controller? Code needed in multiple places should not require redux.
> Asynchronous actions feel, to me, that they belong at the component layer where niceties such as spinners are being rendered, and then the backing store is updated with the results of the triggered action.

This is exactly how thunks work, you just fire them from your component as a Redux action. I use thunks primarily for when an action needs to get data from an API and insert it into the store (e.g. on login).

Inverting the question, why would I want my presentational components to do a bunch of extra work beyond presenting? Calling an API is not presentational.

I think calling components "presentational" is an abuse of the term. It is all imperative facade at any meaningful level. The relationship of components to the user interactions triggered through them is not materially, to me, different from issuing a command from a CLI. As such, I tend to constrain imperative, this-can-fail activity to that layer rather than chucking it into my datastore.
The beauty of Redux and react is that you can do that and nobody's going to tell you you're bad for doing it.

Even Dan Abramov did it in his recent Redux presentation.

It's important that we understand why "best practices" exist but to never see them as commandments. Do what makes sense for your case, but it's your own funeral if you're deviating from best practices without having fully thought it through first.

I'm not saying anyone is "bad", I'm saying that I don't see why it makes sense to include the responsibility of acquiring data into the thing that stores data.
Part of the idea behind Redux is to keep components as pure as possible and limit side effects to one place, usually actions. You keep all async actions in components?
I write separate business logic into which are passed their dependencies and/or the data to operate on. If you have a defined API client, you can pass that to your business logic and let it work with the API client, but to do so means that in testing you have to mock your API client. So my business logic objects tend to work on groups of domain objects (in a functional manner, usually using ImmutableJS these days).

Functional-core, imperative-shell design suggests to then use this functional core within a shell of code that handles IO, handles failure cases--effectively, anything that can fail belongs here. To me, that's the React component; I don't view React as "functional", as it's got multiple types of state even separate from Redux (and this is not a demerit, IMO). The onClick handler of a React button is no different from the command handler for a CLI application. The code in that handler makes requests of its API client (which I typically inject through context and my own HOC--like I said, React's statefulness doesn't bug me at all), it performs business logic functions on the data yielded from its API client, and it stores the result (typically with a onComplete method or the like, so my redux-connected component can dispatch an action).

I'm amenable to other ways to do that data routing (because that's really all any of this is), I just don't see why the state store is where this routing should be done and see a lot of reasons around testing and failure-rescue and general code cleanliness why I wouldn't. If you were to make HTTP requests in your SQL datastore I'd be remarkably concerned for your sanity; I don't think it makes any more sense to do so in a Redux store.

_Can_ you put actions in a proper redux store? I thought the whole point was to keep them distinctly separate.
That's effectively what redux-thunk seems to want you to be doing: running asynchronous operations inside of its control.

That makes me super nervous.