Skip to content

Conversation

@toddself
Copy link
Contributor

Call Object.freeze on state when passing it into to the reducers or effects
so that state cannot be modified except when being returned from an emitter.

Add tests to prove state is immutable in these scenarios.

@toddself
Copy link
Contributor Author

So! The current tests run in node which means you can't call send.

So I made a test file that you can browserify and run in the client.

However, setting up automated testing for browser tests is super opinionated so I didn't set up a way to actually, you know, run the tests.

We use karma for scripto for client testing (and xvfb for doing it headless on CI). However, karma is very heavy and requires a TON of modules and plugins. but it also is extremely reliable, and gets you coverage reports easily. But, since it's also XVFB for automated testing, I still can't get it set up correctly on travis-ci (but can do it on circle-ci).

I've also done testling, which is super minimal, but doesn't get you coverage easily and still requires XVFB.

Zuul is great, but you can't run it in an automated fashion without setting up sauce labs support (I have an extremely old branch where I tried to implement local test running but never finished).

So like, um, I have no clue how you want to commit to running these tests, but like this will open it and run it...

echo '<title>test</title><body><script src="bundle.js"></script>' > index.html
../node_modules/bin/browserify browser.js > bundle.js
open index.html

(Or xdg-open if you're on linux. I have no clue how to do this on windows lol)

@yoshuawuyts
Copy link
Member

Ooh, love this patch - minor version I reckon. Just needs tests fixed and we can merge.

Browser tests are def tricky - perhaps just add these tests for now, but exclude them from CI? - that way Travis stays green and we can save ourselves some work in the future.

I've done a bit of work for tests / coverage here - #24. It uses smokestack which can integrate with saucelabs; I reckon using sauce might be the easiest?

@queckezz
Copy link

queckezz commented Jun 15, 2016

Another alternative to consider might be tape-run. Basically runs your code through a headless electron instance. No integration with saucelabs ofc though, so v8 only. I run it like this:

browserify --debug test/index.js | tape-run --render="tap-spec"

Edit: Sorry, should have written this into #24

Call Object.freeze on state when passing it into to the reducers or effects
so that state cannot be modified except when being returned from an emitter.

Add tests to prove state is immutable in these scenarios.
@toddself
Copy link
Contributor Author

Oooooh I hadn't seen tape-run and it's instructions for using electron! That would greatly simplify some other only-chrome stuff in my life :).

Interestingly, you can't easily mark a test as "skip" when you run tape tests through the node executable. Adding {skip: true} at either level makes node simply exit after running browser test (since it comes first in the glob), and even wrapping it in an if statement causes node to exit after processing the file.

I renamed it so it comes later in the glob pattern (and since node will exit after the first file it will never execute), but this feels really hacky?

Better ideas or solutions?

Also added some documentation to explain that state is frozen within a reducer or an effect just to be explicit about it.

@toddself
Copy link
Contributor Author

So I tried to use Zuul since then it can run in sauce labs for free using "open sauce" which means you can target a ton of browsers, but Zuul uses an older version of browserify-istanbul, which relies on an older version of esprima which is unable to parse template tag syntax.

I opened a PR to update to the latest version of browserify-istanbul, but that's a dead-end until then defunctzombie/zuul#289

I do have this running under karma in my project, but the list of requirements is pretty heavy (although some sub-deps of choo require karma already so we're installing it as it is).

Here's the minimal list I've found I need to just support tape and istanbul via browserify:

    "browserify": "^13.0.1",
    "browserify-istanbul": "^2.0.0",
    "istanbul": "^0.4.3",
    "karma": "^0.13.22",
    "karma-browserify": "^5.0.5",
    "karma-chrome-launcher": "^1.0.1",
    "karma-coverage": "^1.0.0",
    "karma-script-launcher": "^1.0.0",
    "karma-tap": "^1.0.4",
    "tape": "^4.5.1",

That feels kind of heavy, but it works. Except for in travis-ci.

@yoshuawuyts
Copy link
Member

This patch is looking great by the way - testing, merging and publishing as minor ✨

@yoshuawuyts
Copy link
Member

oops; so the http example is failing - digging in

@yoshuawuyts
Copy link
Member

yoshuawuyts commented Jun 20, 2016

Ah yeah, so the problem here is that Object.freeze mutates an existing object, rather than create a new copy - the fact that a single action can trigger both an effect and reducer is problematic - I think the cleanest resolution is to only allow for a single call - that would be a breaking change.

I think we should clean this up and publish in v3. @toddself would you be willing to do that?

@yoshuawuyts yoshuawuyts added this to the 3.0.0 milestone Jun 20, 2016
@yoshuawuyts yoshuawuyts mentioned this pull request Jun 22, 2016
@anandthakker
Copy link

@yoshuawuyts @toddself Hey, just a heads up -- I've lost the reference to where I read about this, but I distinctly remember reading that Object.freeze has some pretty bad performance characteristics in some browsers (maybe Safari? not sure). Would you consider guarding this with a check for process.env.NODE_ENV === 'production', so that it could be easily stripped out of a production build?

@anandthakker
Copy link

the fact that a single action can trigger both an effect and reducer is problematic - I think the cleanest resolution is to only allow for a single call

@yoshuawuyts interesting; could you say a bit more about this? If I'm understanding you correctly, this would effectively remove "actions" as a concept independent of effects and reducers--is that right?

@yoshuawuyts
Copy link
Member


Object.freeze has some pretty bad performance characteristics in some browsers (maybe Safari? not sure).

Oh yeah, that's a good call. We could def add an argument that can be passed on start - I've found it's generally not a good idea to have env var switches live within packages.


If I'm understanding you correctly, this would effectively remove "actions" as a concept independent of effects and reducers--is that right?

Ummm, maybe? The way I view actions is as a single trigger on any of the stores - a 1:1 mapping if you will; quite similar to a function call. Having it trigger multiple things at once seems... well confusing haha


Hope this answers your questions!

@anandthakker
Copy link

The way I view actions is as a single trigger on any of the stores - a 1:1 mapping if you will; quite similar to a function call. Having it trigger multiple things at once seems... well confusing haha

Interesting. In my work, I've been thinking of "actions" as a separate abstraction, basically representing any "verb" within the system (some of which map directly to user actions like selectSomeItem or cancelSomething, others of which are more internal like fetchSomeData or updateBlahStatus). Coming from that point of view, I feel that it can sometimes make sense for an action to trigger multiple things, because I think there isn't always a 1:1 mapping between "verbs" and stores. But if there's going to be a 1:1 mapping, then would it be simpler to eliminate actions altogether, in favor of an API like (state, effects, stores) => { ... stores['namespace:reducerName'](args) ... }?

@toddself
Copy link
Contributor Author

I have it set up similarly but rather use effects to trigger multiple reducers to react to input.

The user might perform some action which causes several parts of the state to update, so i trigger an effect which in turn triggers all the single modification reducers to fire that are necessary.

On Jun 24, 2016, at 05:28, Anand Thakker notifications@github.com wrote:

The way I view actions is as a single trigger on any of the stores - a 1:1 mapping if you will; quite similar to a function call. Having it trigger multiple things at once seems... well confusing haha

Interesting. In my work, I've been thinking of "actions" as a separate abstraction, basically representing any "verb" within the system (some of which map directly to user actions like selectSomeItem or cancelSomething, others of which are more internal like fetchSomeData or updateBlahStatus). Coming from that point of view, I feel that it can sometimes make sense for an action to trigger multiple things, because I think there isn't always a 1:1 mapping between "verbs" and stores. But if there's going to be a 1:1 mapping, then would it be simpler to eliminate actions altogether, in favor of an API like (state, effects, stores) => { ... stores'namespace:reducerName' ... }?


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.

@anandthakker
Copy link

Yeah, I can see how that makes sense, although again, if there's going to be a 1-1 mapping between actions and effects||reducers, then it seems to me like the "actions" concept is unnecessary...

@toddself
Copy link
Contributor Author

It's more of one-to-many. One effect can trigger several reducers, but
a reducer only ever changes one part of the state and will never call another
reducer.

On Fri, Jun 24, 2016 at 06:36:12AM -0700, Anand Thakker wrote:

Yeah, I can see how that makes sense, although again, if there's going to be a 1-1 mapping between actions and effects||reducers, then it seems to me like the "actions" concept is unnecessary...


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub:
#46 (comment)

@toddself
Copy link
Contributor Author

Here is an example:
https://github.com/toddself/boxcar/blob/master/effects/move.js

On Fri, Jun 24, 2016 at 06:36:12AM -0700, Anand Thakker wrote:

Yeah, I can see how that makes sense, although again, if there's going to be a 1-1 mapping between actions and effects||reducers, then it seems to me like the "actions" concept is unnecessary...


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub:
#46 (comment)

@anandthakker
Copy link

It's more of one-to-many. One effect can trigger several reducers, but a reducer only ever changes one part of the state and will never call another reducer.

effects-reducers is 1-to-many, but I thought @yoshuawuyts was proposing above that actions-{reducers or effects} should be 1-1

@yoshuawuyts
Copy link
Member

(...) actions-{reducers or effects} should be 1-1

Yup you're both right. One action can trigger either one effect or one reducer; effects can emit multiple actions. Not the current behavior, but think it would make more sense

@anandthakker
Copy link

Yup you're both right. One action can trigger either one effect or one reducer; effects can emit multiple actions. Not the current behavior, but think it would make more sense

@yoshuawuyts I can definitely see your point on the clarity/simplicity of doing this, but it still leaves me with the question about what value is then being added by the "action" concept? Since each action would simply map directly to either a reducer or effect, they really are equivalent to function calls (like you said above) -- so at that point, why is the indirection through send(...) necessary or preferable to an API where you just call methods directly?

@yoshuawuyts
Copy link
Member

why is the indirection through send(...) necessary or preferable to an API where you just call methods directly

Because multiplexing. A function call requires knowledge of which function you're calling, and importing it explicitly from the location it's declared. By using a message bus, function calling becomes centralized and thus more loosely coupled. This enables better tracing, logging and upgrading parts of the application without touching other parts. It also clearly signals that it's a different method of calling functions; one where callbacks or returning eventual values doesn't fly - thus making for a better enforcement of the unidirectional data flow (despite there always being ways of cheating around it).

@anandthakker
Copy link

A function call requires knowledge of which function you're calling, and importing it explicitly from the location it's declared. By using a message bus, function calling becomes centralized and thus more loosely coupled. This enables better tracing, logging and upgrading parts of the application without touching other parts.

Ah, yeah: I totally agree on the value of a message bus internally. What I was thinking/wondering about was not plain function calls, but rather an API where instead of getting send as a parameter, views (and effects, etc.) would get something like { effects, stores }, and where stores['thing1'](data) or effects['thing2'](data) would be the exact equivalent of send('thing1', data) or send('thing2', data)...

It also clearly signals that it's a different method of calling functions; one where callbacks or returning eventual values doesn't fly

... but I hadn't thought of this point, which I can see being a downside of the thing I'm describing above. I guess now that I've written it out more explicitly, the thing I was suggesting adds more magic and more internal machinery for little gain.

Thanks for engaging with my speculations and questions!

@toddself
Copy link
Contributor Author

@yoshuawuyts agreed on the fact that Object.keys(reducers).concat(Object.keys(effects)) should be a unique set otherwise it makes it hard to reason about order of operations when you trigger something that fires and effect and a reducer.

@yoshuawuyts
Copy link
Member

@toddself Given that this is moved to https://github.com/yoshuawuyts/choo/tree/browser-tests and barracks I think it's safe to close right? ✨

@toddself
Copy link
Contributor Author

💯

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants