Closed ntilwalli closed 7 years ago
How did you notice the odd state$
emissions? I'm asking because I would expect the state$
to catch up before the screen is even drawn the first time, so that no flash can be seen.
There are many cases in which a complex network of streams can emit superfluous events, but that's fine as long as the right events are emitted at the right times, and the state of the app is always consistent, if odd at moments. Javascript is synchronous, it has no such thing as simultaneous events. We can't overcome Javascript's limitations, but by using streams, which operate at a higher level of abstraction, we should be able to ignore those limitations. The order in which streams are merged shouldn't change the overall behavior of the app. The question is then: why can't you ignore the superfluous emissions of state$
?
Personally I'd like to keep the possibility to omit the default reducer in a component.
The question is then: why can't you ignore the superfluous emissions of state$?
You can't literally ignore superfluous emissions of state. You can't pretend they don't exist. My view functions assume the truthiness of certain pieces of state. When those pieces of state are invalid/not-truthy, the view crashes. This is not about avoiding a flash. It's about a crash.
To "ignore" in this context means...
1) Knowing they happen (which, in a sense means you're not ignorant) 2) Using a technique to filter on them
The technique I tend to use is proper merge stream ordering and when that fails, adding truthiness guards in my view functions (and sometimes adding a state-validity flag). These are things I didn't have to do when I maintained my state local to my components.
Am I missing something? Is there a way to actually pretend invalid states don't exist without causing crashes? Maybe using requestAnimationFrame
in a clever way offers an approach?
I can see how omitting a default reducer can be useful. I'm just trying to think of ways to avoid invalid states. At the very least, this issue should be documented.
By "ignore emissions" I mean "be indifferent to emissions", i.e. robust. I think it's a common situation, I run into something similar every now and then. Anyway I think your problem should be solved differently.
Your parent component assumes the state to satisfy certain requirements, which are enforced by its own default reducer, but at the same time it merges its reducers with the two children reducers in a way that doesn't determine their relative order. By using merge
you can't know (in theory) in which order the reducers will emit. That's the problem. Your component needs its default reducer to emit first, but does nothing explicit for that to happen.
This should explicitly enforce that the default reducer emits first:
const defaultReducer$ = ... // the local default reducer
const otherReducer$ = ... // other local reducers
const subsequentReducer$ = O.merge(A.onion, B.onion, otherReducer$)
return {
...
onion: O.concat(defaultReducer$, subsequentReducer$)
}
Perfect. That solves the issue elegantly. concat
has not been a goto operator for me, but it will be now! 👍
Cool that abaco already answered it. I would also add a .take(N)
to each participating stream in the concat
, just to make sure that concatenation actually happens instead of hanging forever if one of the participating streams doesn't complete.
Even though this is closed/answered, I'm still curious about the "can't ignore state emissions" part. I would also say like abaco, that state emissions can be ignored, they should be skippable. If you have some boolean flag and you are creating an event (e.g. component instantiation is an event) whenever the boolean flips, that seems to me like converting from state to event, while in the first place some event caused the boolean flip. So you could theoretically shortcut, avoid the state as an intermediate, and just have a direct dependency between first event and second event.
For example:
const click$; // event
const toggle$ = click$.startWith(false).scan(prev => !prev, false); // state
const alert$ = toggle$.filter(Boolean).map(() => 'alert!'); // event
alert$.subscribe(x => window.alert(x));
simplified to
const click$; // event
const alert$ = click$.map(() => 'alert!'); // event
alert$.subscribe(x => window.alert(x));
@staltz What does it mean for a state emission to be skippable
? Do you mean, it's okay to filter state emissions?
I'd answer a soft yes, not a hard yes.
It would be nice if
state$
emissions for a component get filtered until the default reducer for that component gets run... Is that possible?Below is an explanation of one of the reasons that would be useful...
In general, one of the things I've noticed with onionify and RxJS (and I assume this applies to xstream as well) is the importance of
onion
stream merge ordering. For example if I have a componentFoo
which has childrenA
andB
, then the full set of reducers coming from Foo is the merge of local reducers fromFoo
and the reducers coming from childrenA
andB
. If I write that as:Then the "merged" reducers stream, (which conceptually has no ordering), will get subscribed in the order given. So
A.onion
will get subscribed first, thenB.onion
, thenlocal_reducer$
. This means that very likely the localdefault_reducer
will not be run before the childrens'default reducer
streams are processed which temporarily causes oddstate$
emissions forFoo
(...and to it's children if the parent state information gets lensed down). This invalid state persists until all the reducers from the merge have been subscribed and thestate$
catches up, i.e. all thedefault_reducer
streams associated with the merge have been processed.In general being able to assume that the default reducer for a component has been run before the
state$
for a component emits is a convenient property since it means I don't need to write defensive code within the parts of my component which read thestate$
. Of course it is possible to add a filtering flag to the state indicatingonion_valid: true
, but that's hacky and ugly.It should be noted, and strongly, that this issue can be avoided by simply placing
local_reducer$
as the first entry in the merge. SoO.merge(local_reducer$, A.onion, B.onion)
does not have this same issue since the parent's default reducer will be subscribed before the default reducers from the children. Easy enough, but it's not intuitive...The non-intuitiveness is why I'm raising this as an issue.