Hacker News new | past | comments | ask | show | jobs | submit login
JavaScript Power Tools: Real-World Redux-Saga Patterns (formidable.com)
138 points by thekenwheeler on June 8, 2017 | hide | past | favorite | 41 comments



I worked on a fairly large and high traffic React app. It had some complicated async flows, so the team moved from redux-thunk to sagas.

The claims that testability is improved do not ring true to me. Some of those tests were just awful to write and maintain. I'm sure someone will say "you were doing it wrong" but there were many folks on the team following advice from the maintainers themselves on Discord. General understanding of Sagas by less seasoned developers was also worse than trying to teach them promises.

After the codebase was converted, the team generally concurred that sagas were a waste of time and effort, especially with async/await gaining steam. They also felt that maintainability and debug-ability suffered with sagas. Reasoning about what code runs next is easy, until it's not.

YMMV, but I've tried all the async patterns, and I think sticking with async/await and promises is probably the best bet these days. The code might not "look" as pretty - but aesthetics should hardly be the main goal.


> After the codebase was converted, the team generally concurred that sagas were a waste of time and effort, especially with async/await gaining steam.

To be fair, I'd almost certainly recommend async/await for simpler use cases- in particular, an application in which API calls are the only async behavior. However, in our situation, redux-saga turned out to work pretty well. We needed to coordinate some relatively complex UI flows (the user would draw on a map, then the app would POST the coords to the server). Before introducing redux-saga, we were starting to get lost in a daisy-chain of Redux actions.

> Reasoning about what code runs next is easy, until it's not.

This was the general idea behind "navigationSaga"- to set things up so there's as little as possible happening at a given time.

> The claims that testability is improved do not ring true to me. Some of those tests were just awful to write and maintain.

I dunno. On one hand, I wish there were better tools for testing sagas, but on the other, testing long chains of Promises has caused me all kinds of headaches in the past, as well. I find that it's easier for me to keep responsibilities separate when using sagas than it is using promises or suchlike.

And for what it's worth, I'm not trying to bill sagas as a be-all-end-all solution for all life's problems. :) I was just trying to sorta explicate an approach that worked for us.


It sounds like you guys made the right moves - trying promises first until that fell apart, and then carefully picking a new path forward.

My comment was mainly intended as a "cautionary tale" for those folks who get really excited about the latest and greatest, and don't apply that base level of skepticism to new tech.


Any claims about scalability or testability mean nothing without concrete code examples to back them up. Any tool that makes claims such as these without a sufficiently complex example in the docs, alongside a baseline implementation for comparison, is a huge red flag IMO.


We have been trying elm, and while it has still poor support for "web API", maintaining and growing an elm app has been the most pleasurable and easy thing that happened to us in a long time.


Interesting. What specific concerns or pain points did you experience? Any examples of "reasoning about what code runs next is easy, until it's not" ?


The put/call/takeLatest/takeEvery effects, coupled with all the yields made things impossible to grok for the less seasoned folks. Even for me, having seen some similar things before with other libraries in other languages, it's just not a pattern that my brain wraps itself easily around. There is so much indirection between the sagas and reducers and the effects. Like, I just want to see the code that actually does all the work. I understand the sagas "look" easy, but that's just because all the good stuff is hidden away. I like to see easily where the rubber meets the road, so to speak.

The main selling point was "look how easy this code is to read, it's async but you can read it procedurally". This is mostly true for simple sagas, but the advanced flows were challenging to read, especially trying to remember what each effect did, and what "real" code it called under the covers.

Thinking back on that code, had async/await been used - it would have been simpler to work on and understand, and you wouldn't have carried the weight of a third party library with somewhat esoteric ergonomics.

Edit: It's been several months since I worked on that code, and it's all Angular 2,4,a billion all the time now, so I apologize for limited specifics. In general I appreciate that people are trying new async things (CSP, sagas, observables etc) but they all just feel like a fad to me. Obviously use the right tool for the job, but most of the time I think promises should be the way to go unless there's a super compelling reason not to.


My gripe about sagas is that it mostly reimplement observables-like APIs on top of generators. Observables do take a bit to learn, but once you learnt them, the knowledge is more or less reusable across platforms (even if it's a little different, you can do Go, Scala/Akka, Elixir, and any language that has an Rx implementation).

With Sagas, while the upfront cost and the gain are in the same ballpark for Redux apps, that's basically the only place you'll use the investment. You also cannot leverage the collective knowledge and patterns of the tens of thousands of engineers from other platforms.

That makes it a lot less palatable to me, even though they are easier on the eyes at first, especially for people with mostly procedural programming backgrounds (which is the more common case).

Unlike the initial statement, I do think they're easier to test than even Observables/Promises, because generators make it so easy to inject results and dependencies anywhere in a complex flow. But marble tests aren't bad either.


> There is so much indirection between the sagas and reducers and the effects. Like, I just want to see the code that actually does all the work. I understand the sagas "look" easy, but that's just because all the good stuff is hidden away. I like to see easily where the rubber meets the road, so to speak.

So do I, which is why I dug into it a bit deeper before using it on a project. This article is actually the third in a series (see [1] and [2]), and the first one goes into a little bit of detail about how things work behind the scenes.

> The put/call/takeLatest/takeEvery effects, coupled with all the yields made things impossible to grok for the less seasoned folks.

I definitely feel you on this, and it's _very_ easy to let things get out of hand. I'm very much _not_ a fan of nesting an anonymous generator function inside takeEvery/takeLatest; it's just too easy to start accessing variables from the enclosing scope and then everything gets totally FUBARed.

> It's been several months since I worked on that code, and it's all Angular 2,4,a billion all the time now, so I apologize for limited specifics. In general I appreciate that people are trying new async things (CSP, sagas, observables etc) but they all just feel like a fad to me. Obviously use the right tool for the job, but most of the time I think promises should be the way to go unless there's a super compelling reason not to.

This is totally fair, and I definitely agree that apps should start with simple tools and only use power tools when they're called for. As I mentioned in my other comment, we started out with something like Promises, and moved to redux-saga when that approach was starting to get cumbersome.

1: http://formidable.com/blog/2017/javascript-power-tools-redux...

2: http://formidable.com/blog/2017/composition-patterns-in-redu...


> After the codebase was converted, the team generally concurred that sagas were a waste of time and effort, especially with async/await gaining steam.

Aren't sagas and async/await completely orthogonal issues or am I missing something?


This is an excellent series of posts! Parts 1 and 2 give a ground-up description of the core concepts that redux-saga uses (generators, describing effects, etc), and Part 3 covers some real-world use cases. One of the best tutorials on redux-saga I've seen.

If anyone's interested, I do have links to other tutorials and articles on redux-saga in my React/Redux links list, at https://github.com/markerikson/react-redux-links/blob/master... .


A whole class of problems you wouldn't have in the first place with MobX. So much code in the article for doing so little - where good design could solve the issues much more elegantly using plain function composition and JS concepts.


Author here.

As I mentioned in other posts, I don't believe redux-saga is the be-all-end-all solution to every problem in client-side Javascript. It's a good solution for codebases which need to coordinate among several different asynchronous processes.

And for what it's worth, I wanted to err on the side of verbosity. I find that a lot of blog posts kinda elide over too much, and so I was actively trying to break down my thought process into very discrete steps. At the end of the day, I think you'll find that the final product involves less than 300 lines of code. If that.

On a different tack, MobX is an excellent library, and we've actually used it to great effect at Formidable! Ken Wheeler has a great post about the topic. The point of this article, though, is simply to share something that worked well for us on a complex project, where replacing Redux with MobX wouldn't have been that helpful.

Along those lines, I ask you in complete honesty: what would you like to see out of a "Javascript Power Tools: MobX" article? I'd love to dig into it a little bit, find out what makes it tick, and share that with folks. :)


I think it would be great for the community to see a writeup on how complex async flows can be handled with MobX reactions and data atoms, two pretty powerful concepts IMO


Agreed. Just refactored a medium sized redux/redux-saga codebase (20 kloc) to MobX. Shaved off a couple of thousand loc of logic and boilerplate in the process, and everything became so much easier to reason about.


I'm going to use this opportunity to plug my Redux Saga testing library: redux-saga-test-engine[0][1]. It makes saga tests much less verbose. Let me know if you like it (or don't)! :)

[0] https://github.com/DNAinfo/redux-saga-test-engine [1] npmjs.com/package/redux-saga-test-engine


This is awesome! I'm definitely going to give it a try on my next side project. :) Thanks for sharing!


I don't understand.

Why not simply observables?


Author here.

Saying "why not simply observables" kinda glosses over a lot of details, but I'll try and answer as best as I can.

I've always found it a bit difficult to use Observables in application-style code, both in a conceptual sense (handling Redux actions as an observable stream) and in a practical sense (actually building and debugging streams and transformations over those streams). I do think they're excellent tools for dealing with cross-cutting concerns like logging, analytics, and debugging. But as I mentioned in the article, "business logic is inherently procedural, and expressing it as such makes our intent clearer." That's the major advantage, I think- being able to use simpler tools (e.g. loops and local variables) in a powerful new way.

And don't get me wrong, I'd love to learn RxJS properly one of these days. :) This article isn't about saying "well this approach is THE BEST APPROACH", it's more about saying "These tools turned out to be super helpful. Here's my thought process, maybe this approach will help you as well."


Yes, this article assumes (rightfully) that people are using redux saga and need help with it.

I just found the whole concept of redux and redux-saga a bit strange. It seemed like people don't want to use observables for whatever reasons and try to push Redux to solve these async problems.

Your article is good, I just had the feeling it wouldn't be needed if people would use better suited abstractions in the first place.


I'm still not sure exactly what you're trying to say there, particularly by "push Redux to solve these async problems".

Redux itself is about synchronous state updates, as inspired by the Flux Architecture. Because of that, async behavior and side effects are handled via middleware.

Most developers are not overly comfortable with FP concepts or code. Redux was intended as a lightweight intro to FP principles, and React also helps push people in that direction. Even fewer people are comfortable with observables, although Angular is maybe starting to change that somewhat. So, it's not just about "better suited abstractions", it's about what developers understand and can use well.

To be honest, your comments seem to be leaning towards the common stereotype of an FP enthusiast: "This approach is clearly superior and correct, why would people do anything else?"


> Because of that, async behavior and side effects are handled via middleware.

It shouldn't be though. The middleware pattern makes a lot of sense for transforming data (communication between mismatched APIs etc), or using it in some way before passing it on (logging, analytics etc). IMO it's totally wrong for this use case though.

All redux-thunk is doing is taking some "action", clobbering it, pretending it doesn't exist, and running some arbitrary function that it's passed.

dispatch(higherOrderFunctionThatCreatesAsyncFunction())

is identical to

asyncFunction()

What people are looking for is some way to get the data store dependencies into asyncFunction in a testable way. This can easily be done without middleware.


Yes, yes, we've had this argument about redux-thunk in prior threads :)

That said, it's worth noting that the explicit design goal for middleware _was_ for async behavior, per the comments from Dan and Andrew I've quoted in http://blog.isquaredsoftware.com/2017/05/idiomatic-redux-tao... . To pick out one specific quote from Andrew:

> [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!


Yeah I'm under no illusions that me and Dan Abramov are operating under a vastly different model for how we think an application should be structured. The concept of a data store having a "solution for async" is, to me at least, absurd. It's like asking why my fridge doesn't have a solution for next day grocery delivery.

I don't see why my fridge needs to be concerned about whether my broccoli is sitting on the bench ready to be put in, or still being washed down at the green grocers.

From the fridge's point of view, a piece of broccoli is inserted into it at a specific point of time. The contents of the fridge can be defined as an ordered set of insertions and removals. The fridge's state depends on knowing whether a piece of broccoli was inserted at 12:58pm or 1:03pm, since that may change the order things were stacked in, but it doesn't need to know whether I placed an order for that broccoli this morning, yesterday, or a week ago; or how it arrived at my house. Making my fridge responsible for that process would seem to violate the concept of separation of concerns.


Finally someone sane, thanks.

Funny thing is that Dan himself said Redux could be implemented in 5 lines of RxJS and was also rathere interested in observables til he met some haters on a conf.

I remember him asking a question about observables being the next thing or future or something and the speaker just didn't know what to say.


Because not everyone likes or uses observables? :)

redux-saga was one of the first major side effects middlewares to come out (besides redux-thunk). At this point, I would say that thunks and sagas are the two most popular approaches to side effects in Redux, with observables and various promise-based approaches also used but to a lesser extent.

The main selling points for sagas are things like testability, descriptive declaration of side effects (per Merrick Christen's "effects as data" gist the other day at https://gist.github.com/iammerrick/fc4a677cea11d9c896e8d3a29... ), and the ability to spawn background-thread-like sagas as needed.


Yes, I know.

I just found it strange that people try to bend Redux til it does their bidding and not simply use something that works out of the box.


Well, one of the primary design goals behind Redux was to make async behavior a pluggable approach (as detailed in my blog post "The Tao of Redux, Part 1 - Implementation and Intent" [0]).

So, I don't see how use of redux-saga, or redux-thunk, or any other async middleware, qualifies as "bending Redux until it does their bidding", given that it was explicitly intended to allow that.

On the other hand, there definitely _are_ lots of ways that people "bend Redux", especially things like trying to slap OOP layers on top of an FP-oriented library. Those are technically valid because the Redux core is very unopinionated, but they're definitely not idiomatic Redux usage. (I also discussed those in "The Tao of Redux, Part 2 - Practice and Philosophy" [1]).

[0] http://blog.isquaredsoftware.com/2017/05/idiomatic-redux-tao...

[1] http://blog.isquaredsoftware.com/2017/05/idiomatic-redux-tao...


I used Redux back in the days and async behavior felt rather clunky (action begin, action success, actionFailure) the other approaches felt like somehow getting rid of this clunky stuff.

Using observables instead of Redux doesn't even lead to these fixes.


As I describe in that "Tao of Redux Part 2" post, there's no _requirement_ that you dispatch START/SUCCESS/FAILURE actions as part of your async requests. You'd need SUCCESS at a minimum or an equivalent to handle the actual updates from the request, but the START/FAILURE actions are only needed if you want want to do things like showing spinners or have some kind of undo/redo behavior. It's a useful convention and a common pattern, but definitely not a requirement.


I find observables, rxjs and redux-observable code to be hard to read and step through debug.

Redux saga is very easy to read, works well with the chrome debugger, and plugs into a wide variety of use cases pretty seamlessly.


Ah, no I didn't mean using Redux with observables instead of Redux-Saga, but to drop Redux entirely and use observables.


I've been fascinated by approaches like the following: https://michalzalecki.com/use-rxjs-with-react/

Building on top of those ideas I built something a bit more powerful and feature complete than the example in that blog post, but the exact implementation doesn't matter: having RxJS driving the show is really, really cool.

Async issues? What async issues?


The codebase for redux-saga is a dream to read through: https://github.com/redux-saga/redux-saga


could you actually get anything done in js before es6 !?


Quite happily, ES6 is 90% sugar.


Was that a yes or a no? :D


Any advice on how to compare this with RxJS?


It's largely a question of whether you prefer to write your code in imperative-looking form via generator functions, or pipeline/declarative form via observables.

There's a couple particularly good comparisons of Redux side effects approaches at https://decembersoft.com/posts/what-is-the-right-way-to-do-a... and https://medium.com/react-native-training/redux-4-ways-95a130... , and more comparisons in my links list at https://github.com/markerikson/react-redux-links/blob/master... .


awesome, thanks


What do you like about it?




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

Search: