bowheart / zedux

ATTENTION! This is the old repository. Zedux has moved to https://github.com/omnistac/zedux
https://omnistac.github.io/zedux
MIT License
47 stars 0 forks source link

New side effects model #3

Open bowheart opened 6 years ago

bowheart commented 6 years ago

The existing side effects model aims to be extremely easy to use.

const todosReactor = react([])
  .to(fetchTodos)
  .withProcessors(dispatch => {
    fetch('/todos')
      .then(/* ...dispatch stuff... */)
  })

There are two overarching problems with it:

  1. It is impure.
  2. In some ways, it fails at being extremely easy.

How is it impure?

Processors are run locally - at the reactor level. While this makes it extremely straight-forward, it means that the store (and any parent stores) have no idea what's going on. Good luck finding the far-reaching, asynchronous effects of dispatching an action to the root store.

This breaks composability. And that's bad. Composability is what makes Zedux stores so dynamic and flexible. In order to be truly composable, a store's state and side effects must be completely transparent.

How does it fail at being extremely easy?

While the actual usage couldn't get much easier and boilerplate-free, the current side effects model burdens the learner conceptually. In order to master it, one must learn two completely new concepts: Inspectors and Processors. These two pieces actually add a lot of complexity to understanding how Zedux stores work.

The solution

The current WIP is to kill the inspector and processor layers entirely, replacing them with a single Side Effects layer. This layer behaves similarly to the old Processor layer. But it's pure. This new layer's goal is simply to produce a list of effects for an action, much like the reducer layer's goal is to produce new state from an action.

Effects will be propagated up to the store and can be handled there. All effects will continue propagating up to parent stores, all the way to the root.

This means that Zedux stores will no longer have any processing power built in - no generators or observables out-of-the-box. What?? Yeah. It's sad. BUT:

Zedux will now ship with a factory function for creating effects and an Higher-Order Store for handling effects. A simple Proof-of-Concept:

import { createStore, effect, withEffects } from 'zedux'

const fetchTodos = effect('fetchTodos')

const store = withEffects({
  [fetchTodos]: dispatch => {
    fetch('/todos')
      .then(/* ...dispatch stuff */)
  }
})(createStore())

There's a ton of stuff involved with this change, including changing the meta chain spec to make effects dispatchable (:open_mouth:). But this should be a good enough overview for now.

Thoughts are welcome!

cjk commented 6 years ago

I'd miss the built-in observables, but I guess the new mechanism will provide for similar functionality and the higher goals are worth it. So, yeah, go for it!

cjk commented 6 years ago

Question: Considering your above mentioned WIP, would you recommend using Zedux right now for "serious" projects or should I rather wait until it matured some more (you mentioned a memory-leak somewhere) and then switch from Redux et.al. ? Do you think Zedux is a good choice for handling stores with tens-of-thousands of entries efficiently?

bowheart commented 6 years ago

Good question. Zedux has matured so much, it's easy to forget that it's still in a pre-pre-release (v0.0.20). As such, I think I should technically advise against using it in very serious projects.

That said, I have been using it on quite a few very serious projects for a while now. And it's been awesome. Before this refactor, the api has been very stable for months. But there will be a few changes now.

I don't think refactoring your code would take too long. Unless you're using a bunch of low-level apis, like the meta chain spec, the biggest changes you'd have to make are removing calls to store.inspect() and renaming .withProcessors() to .withEffects():

react([])
  .to(fetchTodos)
  .withProcessors((dispatch, action, state) => { ... })

becomes:

react([])
  .to(fetchTodos)
  .withEffects((state, action) => dispatch => { ... })

which will be a shorthand for:

react([])
  .to(fetchTodos)
  .withEffects((state, action) => [{
    effectType: 'call',
    payload: dispatch => { ... }
  }])

This shorthand would also support observables and generators, btw. And then, of course, there's wrapping all effects-handling stores in a withEffects() HOS. And yes, the HOS has the same name as the ZeduxReactor .withEffects() method. Is that confusing? Maybe we'll change that...

Anyway, Zedux should definitely be at least in a pre-release by now. I think I've just been holding back because the first pre-release will need an "Introducing Zedux" blog post that I'll try to promote on Hacker News and whatnot. And let's be honest, Zedux straight-up competes with Redux. And if you do that, it's gotta be good.

you mentioned a memory-leak somewhere

The memory leak is fixed. I mentioned the fix in the same comment where I mentioned the leak:

Zedux now has an intelligent tree diffing algorithm for crazy-fast hierarchy merging. And no memory leaks.

The tree diffing algorithm is actually pretty awesome, btw. I'll probably write a blog post dedicated to it someday.

Do you think Zedux is a good choice for handling stores with tens-of-thousands of entries efficiently?

Yep. It's designed to be much more efficient than Redux. I haven't actually tested speeds in a while, but some of my Zedux-favored tests showed that Zedux hauled about 3x faster than Redux. And even the Redux-favored tests have Zedux neck-and-neck with Redux.

All speed tests are gonna be a little arbitrary, of course, but it's pretty safe to say that Zedux is at least as fast as Redux. Even if you use Zedux just like Redux - a single global store with a single massive reducer hierarchy, Zedux is at least as fast as Redux.

The real clincher is that Zedux scales like a beast. If you use it well, it will be way more performant than Redux. As your app gets more and more state, you'll naturally start breaking pieces into their own stores. And isolated stores are super performant. Dispatching an action directly to its relevant store is obviously going to be much more efficient than running every single action through the entire reducer hierarchy.

Anyway, there's a guide in the docs for this:

https://bowheart.github.io/zedux/docs/guides/optimizingPerformance.html

cjk commented 6 years ago

That sounds reasonable, thanks for taking your time to respond. It also confirms my good gut feeling I had so far with the library. One reason I asked was that, once I dived into the concepts of Zedux / React-Zedux it felt like quite a bit to learn and understand before becoming productive (perhaps I should've tried the zero-config approach first, but I knew I'd need more soon). Guess I'm beyond that phase now, basics are working and I start to feel the power of this little library. So I'll definitely continue until I hit a hard blocker (which I doubt will happen).

I know it must be hard to compete with an Ecosystem like the React/Redux one, lot's of hype and good stuff around. But then I firmly believe if you stick around long enough people eventually notice how awesome the Zedux-combo is and adoption will start to rise.

cjk commented 5 years ago

Saw the effects-model branch emerge some time ago - wondering now: is it ready for use? Just some docs updates missing or polishing where finding the time for is always hard? (BTW funny how withEffects resembles the new react-hooks API somehow and both having somewhat similar goals).

bowheart commented 5 years ago

@cjk hey there!

You are exactly right. I switched jobs a couple times this year and just lost a lot of my focus on open-source - especially since, honestly, one of the biggest motivating factors for me in doing open-source was to showcase talent to prospective employers (a thing I'm not trying to do anymore). I actually have a couple other things to publish too, including a server-side reactive model and some koa middleware. But Zedux is still alive. It will probably be subject to more changes than I originally planned.

There have been a few huge changes that are mostly done, but are really just waiting on determining the final spec. Maybe I should post issues about the ones I'm not sure on and see what you think. The changes consist of:

These are all pretty massive. They will almost definitely all be released together as the 0.1.0 pre-release.

I've also got some practical suggestions to add to the docs after a year of experience using Zedux. A duck-style approach has been my favorite so far.

React Zedux also has some changes on its plate. It needs to be rewritten in TypeScript and some of the api should be re-thought to work better with TypeScript. In fact, if it could use React hooks exclusively, the whole api would be so much simpler. I would so love to do that. But it may be impractical.

Last thing! In playing with the new effects model I have absolutely loved how Zedux stores can now serve as both streams of state and streams of actions. This has made it especially powerful server-side, but is also a useful escape hatch I have found myself really wishing Redux had. And the new composition power is everything I hoped it would be. Anyway, more on that when I finally get around to documenting everything.

cjk commented 5 years ago

@bowheart

I'm glad you and Zedux are both alive and kicking - and I'm grateful you've always been that helpful and supportive, even while RL-issues are taking their toll and should of course be handled with priority. Can only talk for myself, but I still see a huge potential for (react-) Zedux as a library, even in the light of react-hooks and other recent developments emerging; Zedux brings much more to the table. I'm using it in several projects right now and of course I'd love to see it thrive and get (even) better.

Let me know when I can help with some testing et.al.

I think documentation is already pretty good and the examples (at least the more complex RPG) were helpful in grapsing advanced topics like store-composition. Still it'd be great to have some more practical suggestions on how to make best use of both libraries under certain circumstances!! (I'm probably using only a fragment of it's potential in my projects right now)

Looking forward to try out the new effects-model, just let us know once it's ready for prime-time.