Blogged Answers: Why Redux Toolkit Uses Thunks for Async Logic

This is a post in the Blogged Answers series.

Reasons why I selected thunks instead of sagas as the default async middleware

I was recently asked if I plan on supporting anything besides thunks for async logic and side effects in Redux Toolkit.

At the moment, I only plan on having explicit support for thunks, because:

That doesn't mean you can't use other async middleware. configureStore specifically has middleware and enhancer arguments, allowing you to add whatever async middleware you want as part of your store setup, same as the base createStore.

FWIW, I've used sagas before and think they're a great power tool for complex async logic. I just don't think they're the right tool to be forcing on folks as a default, and most apps don't need them.

Note that the new createAsyncThunk alpha API specifically generates those async lifecycle action types for you. I suppose you could use that with sagas too, by calling it and reusing the exposed action creators / types.

I've seen folks argue that sagas are "simpler" than thunks. I agree that sagas are much more powerful than thunks, but completely disagree on "simpler".

Thunks are a bit weird to wrap your mind around at first ("I pass a function to dispatch? which then calls it and gives me back dispatch again? and I have to write a function that returns a function?"). But, the middleware is only about 10 lines long, so if you look at it once or twice you get what it's doing. Once you use that pattern a couple times it's repeatable, and from there you can write whatever logic you want. Having async/await in particular makes writing async thunks a lot easier to deal with.

With sagas, you have to:

  • Understand what generator functions are and the declaration syntax
  • Understand what the yield keyword does
  • Make sure you call
  • Write extra action types just to kick off the actual saga logic
  • Split your saga logic into "watchers" that listen for a given action type, and 'workers" that do the actual behavior-
  • Read and understand the list of Redux-Saga effects like call(), put(), and fork()
  • Understand that these effects aren't actually doing anything themselves
  • Deal with nuances of yielding different types of values in sagas
  • Grok the saga-specific forking model

And on top of that, from what I've read, sagas and TypeScript don't currently go together well.

Again, I like sagas. I think they're a wonderful tool to have available. Sagas are great for very complex async workflows. In an app I built a few years ago, we had to kick off a bunch of requests that started ongoing calculations on the server, check on the server's completion status for each one, kick off more batches of requests, handle pausing the sequence, handle jumping ahead in the sequence, cancel requests if needed, and so on. No way we could have done that with thunks.

I also get the arguments about sagas being more testable, more declarative, etc. Also valid points.

But for just doing your typical CRUD fetches? Complete overkill. The amount of code you have to write to make that happen, and the number of concepts you have to deal with, make them the wrong choice for that kind of use case.

My goal with RTK is to simplify the most common use cases that users are dealing with, and provide an opinionated set of defaults. My opinion is that most apps shouldn't be using sagas unless they have a clearly demonstrated need for truly complex workflows, and that thunks are the right choice as a default option.

Finally, there is already an open issue to discuss what a more "declarative" side effect approach in RTK might look like. We'll see if anything actually comes out of that discussion.

This is a post in the Blogged Answers series. Other posts in this series: