Conversation
Just to see that I have read this PR correctly, you are using three separate
So unlike how one uses
The last one is one that users got wrong a lot of the time in the lifecycle-based paradigm, which led to this new "sync-state-to-sideeffects"-model that flushes out bugs and edge cases early. That's also a part of why it is rough to use. You wrote on Twitter:
This doesn't mean that complexity has gone away though, only that it has moved. I'd almost like to reformulate that into:
I'd say these recent tweets by Sebastian Markbåge aligns pretty well with this approach, just that in this library, this happens in the reducer in a (hopefully) modelled way, which I think is nice. While I think there might definitely be nuances and pitfalls to this approach which might make it easy to introduce bugs in some cases if you don't model things correctly, that exists with While I don't have a clear enough mental model of Concurrent Mode yet to judge the approach or implementation from that perspective (and I do worry..), it looks good to me! 💯 |
Let's take the timeout example. What really is stopping a timeout before starting a new one, in this case? To me, it seems like replacing an effect. As an enhancement, we can have // start timer
return {
...state,
timer: exec(/* setTimeout */)
}
// restart timer
return {
...state,
timer: exec.replace(state.timer, /* setTimeout */)
} That would be preferable, I suppose, to this (more explicit): // restart timer
exec.stop(state.timer);
return {
...state,
timer: exec(/* setTimeout */)
} What do you think? |
I like that! Not so much because the API is nicer (it is), but also because its existance encourages the user to think about those cases and make sure they are handled. "Why would I need to replace an effect? Oh, right." I guess the one worry I'm having a hard time articulating with the API is that it seems very simple on the surface, but requires some specific patterns to avoid bugs. It kind of looks like a more high level API than it really is, but I think that can be mitigated by docs (esp patterns!) and perhaps warnings? Would a warning when you are execing a parameterized effect that is already running be helpful in some cases? Could you detect in dev when a running entity in the state was replaced by a new one without first stopping the old one? I'm just thinking out loud. 😀 |
A concrete example of what I mean is that your current dog example looks very reasonable to me. I dont intuitively understand that the fetchDog-effect works like an actual effect and can get stale, needs stopping/replacing etc and has a race condition. I think for some people, who might not read the docs, the lure of this library could be to avoid that complexity entirely and move to the simpler (but buggier) mental model of the past. That's not a reason it shouldnt exist or isnt a nice abstraction, just pointing out a perceived challenge. Perhaps this library could evolve to get it's own lint-rules someday? 🤔 |
I'll merge this in as-is (after docs, etc.) and update the example.
There's footguns with either approach. It's way too easy to forget to dispose effects with |
This PR adds support for cleaning up effects by introducing "effect entities", which are objects that are returned from
exec(...)
and hold the current state of an effect:EntityStatus.Idle
if the effect hasn't been executed yetEntityStatus.Started
if the effect has been executedEntityStatus.Stopped
if the effect has been disposedAn effect's disposal function can now be returned from the effect implementation, just as in
useEffect
:The difference between how effects are disposed in React and how they are disposed in
useEffectReducer
is that the developer has full control over the "lifecycle" of the effect, by maintaining a reference to that effect entity:exec(effect)
queues the effect for starting (inuseEffect()
) and returns an effect entityexec.stop(entity)
queues the effect represented by theentity
for disposalAdditionally, all running effects will be automatically disposed when the component is unmounted.
Closes #1