I’m using `StateFlow` in a Kotlin/React project an...
# flow
m
I’m using
StateFlow
in a Kotlin/React project and noticed an interesting nuance that may make it unsuitable for providing state in React-like projects - and maybe others. Am I missing something or do I have an error in my thinking here? In my case the problem crops up in the sign-in process. To simplify, there are just two pages: - The
MembershipPage
shows information about the signed-in user. If no user is signed in, redirect to
SignInPage
. - The
SignInPage
allows the user to sign in. If a user is already signed in, redirect to
MembershipPage
. A single
UserController
component wraps these (and other) user-related pages, keeps track of the signed-in user and provides that value to pages, and performs the redirects if needed. Various React components keep track of the signed-in user by accessing
StateFlow.value
when first rendering the component, and collect
StateFlow
emissions of new values to account for changes. Now when the user signs in the following happens: 1. Send credentials to server. 2. Server responds with OK and user object. 3.
StateFlow
value is set to signed-in user (
flow.value = user
). 4.
SignInPage
redirects to
MembershipPage
. (user has signed in successfully) 5.
UserController
redirects to
SignInPage
. (not signed in) 6.
SignInPage
redirects to
MembershipPage
. (already signed in) 7.
UserController
redirects to
SignInPage
. (not signed in) 8. <repeats a few times> 9.
SignInPage
redirects to
MembershipPage
. (already signed in) 10.
MembershipPage
is presented. There’s a redirect cycle that gets broken automatically after a short time. What’s happening here? After a successful sign-in the `StateFlow`’s
value
is set to the signed-in user. From that moment on
flow.value
returns the signed-in user that was just set. The `StateFlow`’s emission of that new value however is not immediate and takes a while. During steps 4 through 8 above there is an inconsistent state in the React components. Those that happen to use
StateFlow.value
acknowledge that a user is signed in. Those that wait for the `StateFlow`’s emission of a new value still consider no user to be signed in. Depending on whether a component is first rendered (accessing
.value
) or rerendered (value depends on
.onEach/.collect
) the behavior is different. So by using
StateFlow
the system inadverently depends on two different sources of truth for the signed-in state. That’s because the return value of
.value
and
StateFlow
emissions of new value are not synchronized. I’m not sure if that’s intentional behavior of
StateFlow
. State should have a single source of truth and
StateFlow
itself is inconsistent here. One potential solution would be to change how
StateFlow
works. For example by having
.value
to not return a new value until the emission of the new value has begun. There’s still a potential for race between different contexts but at least in synchronous environments like JS that should be irrelevant. There are two possible approaches to use
StateFlow
as-is as a single source of truth: (1) Rely on
.value
only. (2) Rely on value emissions only. (1) doesn’t work because components need to know when the value changes in order to re-render. Otherwise the UI would get out of sync with the state. (2) doesn’t work because components need to access the current value synchronously at the time of initial render. Otherwise the UI would always have to briefly display a loading state while waiting for the initial emission even though that value is already available synchronously. As a compromise I could always rely on
.value
for accessing the current value and use emissions only for triggering a re-render. An inconsistency still remains however. Components aren’t notified immediately when the state they depend on changes. They either render a state that’s potentially newer than the state they were last notified about, or they render a state change too late, and they may render more often than needed. If the
SignInPage
executes some logic as a result of a successful sign-in it does so under the assumption that the user is signed in now. Other components however still assume that no user is signed in. No re-render is pending for them because there’s no notification about a change in state yet. Hence the UI is temporarily inconsistent. I don’t think that this subtle inconsistency between
.value
and emissions only affects Kotlin/React projects. It may also affects other projects which rely on a mix of accessing
.value
and emitted values. If it’s intentional then it should be communicated prominently in the documentation. However for me it would defeat the purpose of a
StateFlow
for managing state because it’s not a single source of truth. I could also write a wrapper that may use a
StateFlow
under the hood but synchronizes the current value with the emissions. But that’s just reinventing what I assume
StateFlow
is supposed to provide in the first place. What are your thoughts on that?
e
This an important consequence of
StateFlow
being an asynchronous source of state. It requires a different mental model as opposed to similar synchronous callback-based APIs.
m
I agree. But why is setting
.value
synchronous then? It should also be asynchronous.
Could be a suspending
.setValue(value)
and a non-suspending
offerValue
.
Then the component could wait for the state update to settle before continuing (e.g. performing the redirect)
e
Setting value cannot be async, while emission cannot be sync. So, you should never be using both value and emission. Any specific use shall rely only on one or the other, but not both.
m
In that case that should be prominent in the documentation. That’s very unexpected if you just look at
StateFlow
API (and current docs).
e
The question on using it with React is need an important one to address. I don't have answer, atm. Will think
That is also a good point to be explicitly expressed in the docs.
m
It’s not just React. I’m not sure how Compose works but it’s similar to React so it may have the same issue.
Wouldn’t it be possible to have a suspending
setValue(…)
that won’t return until emissions are starting [or value was overwritten again]? That would probably solve it for React.
e
That might do the trick.
It should be possible to suspend until all subsribers receive the value. Still error-prone as it cannot wait until done. Will work for react, but...
m
If it waits for all emissions it may deadlock, doesn’t it?
e
It may.
But "first emission" is way more error-prone in other ways.
m
Also there’s no guarantee that it will actually be emitted if another value update comes in quickly.
e
Compose also starts with a value snapshot by default...
However, coming up with a problematic scenario there might more difficult.
m
Maybe the implementation of
State<…>
normalizes the behavior for Compose.
I wonder how it works on Android.
e
You don't have redirects in compose. If you just render stuff (no redirect), then it should not be causing any issues.
m
What about Compose Web?
e
Redirect is alien to state-handling. You must not be doing "destructive actions" (like redirect) based on state.
👍 1
these are what events for...
👍 1
m
It’s not destructive. Redirect is merely an HTML5 API. The URL changes. URL change is merely a state change. The URL could also be tracked in-memory.
history.replace()
etc.
The app remains running - no re-mount needed.
e
but then it shouldn't cause state to reevaluated and flipped to a different value. A "view of state" must be consistent.
m
The URL is state. It changes as an immediate response to “I’m signed in now”. However the rerender caused by the URL changing means re-evaluating state which is not in sync yet because the emissions haven’t happened yet.
e
I see... in your app you effectively have two sources of truth for "if user has logged in". You should somehow make sure there is only one source of truth for this info.
m
I cannot use the URL as source of truth. I cannot use
StateFlow
emissions because they’re async.
So right now I use
.value
as source of truth and emissions just to trigger rerenders. Not ideal but better than the issue I had
e
How this happens?
SignInPage
 redirects to 
MembershipPage
. (user has signed in successfully). What is the source of this decision?
UserController
 redirects to 
SignInPage
. (not signed in) What is the source of this decision?
How comes those two pages have a different view on whether user has signed in or not?
m
SignInPage
was basing it as a result of the successful authentication which has set
.value = user
and thus
.value
reports “signed in”. Subsequent renderings of
SignInPage
saw
.value
is set to a user so triggered subsequent redirects. The redirect for
MembershipPage
was coordinated by
UserController
which was mounted already, so it had to rely on Flow emissions to notice an update of the `StateFlow`’s
value
e
I see.
m
I’ve now changed it like this. It’s a bit weird but works in that case. Only issue is that I have a double render. First I re-render because of URL change. Then I have a re-render because of the emission which happens delayed. But the returned value is always up-to-date now.
It’ll update the components internal state but never reads it. Merely to cause it to re-render (delayed).
e
In "event handling" this is sometimes known as a "glitch". Effectively, you've implicitly created a dependent value. You are assigning "state.value" and issuing a redirect (a dependent value) based on this value. Now another component of the system observes a change in dependent value (if is being asked to render because of the redirect), but it still had not observed the change of the original value, because it is being propagated asynchronously using a different data flow path. This is a glitch -- you saw an effect without seeing its cause.
m
In that particular case yes. But another component could have caused a re-render in exactly the same moment. That re-render would also have inconsistent state between components than use
.value
(initial render) and emission-based
value
.
e
One solution to this is to do redirects using flows, too, as in
originalState.map { mapStateToUrl(it) }.onEach { redirectTo(it) }.launchIn(scope)
. The flows do not really guarantee glitch-freedom (it is hard and computationally expensive) but should work for your case.
m
I cannot map every potential state to a URL. The
SignInPage
only knows in isolation what URL it has to redirect to after a successful sign-in. That URL is typically dynamic too. I was thinking about waiting for the emission of my
.value
that I’ve just set. Like
Copy code
flow.value = user
val user = flow.first()
redirect()
But no idea if that’s reliable.
e
Glitches between states of dependent values is a hard problem. I'm not aware of any easy-to-use and scalable solution.,..
m
However waiting only solve half of the problem. The root cause is still an inconsistency between “current value” and notification about “value change”.
React kinda expects it to be in sync. I call
setState
if/when state changes and I only change state if/when I call
setState
.
e
If your controller "owns" the state and it is the sole component that updates it, then you can wait for an update like this:
Copy code
flow.value = user
flow.first { it == user } // wait for this user value!!!
But this is not realible. It only waits for this subscriber to get the emissions, not necessarily for others.
React is mostly synchronous.
To solve your problem it seems that we really need to introduce some kind of
awaitValueUpdate()
specifically designed for it. (ah... the problem of finding a good name for it)
m
React runs a render in one cycle. If I wait for the first emission to start/happen and then trigger a re-render that queued re-render should happen with all emissions completed. Shouldn’t it?
1. sign in 2.
.value = user
3. await emissions (start is sufficient) 4. redirect (= state change, rerender now queued) 5. rerender (all emissions should have happened here, don’t they?)
I still have the issue that the notification and state are out of sync. Because components rely on
.value
which returns new state before it notified about the change.
e
I'm not that kind of expert in JS event queues. To dispatch coroutines we use a mix of high-priority queue (where promises resolve) and low-priority queue (where setTimeout works). I don't know how React schedules its work (does it always use requestAnimationFrame?)
m
I don’t know either but I don’t think it’s that simple 😅
More helpful would be
.lastEmittedValue
and
.emitAndWait(…)
.
e
.emitAndWait()
might be a good name. but
.value
is the last emitted value....
.emitAndWaitReceived()
might be even clearer
m
No it’s not. It’s the value queued to be emitted. Otherwise React wouldn’t have time to re-render several times with
.value
being different from what was last emitted.
Unfortunately if multiple collectors aren’t guaranteed to be emitted to receive the emission in the same “JS tick” then it’s getting really difficult to solve anyway.
e
Depends on what you call "emitted value". In my mind (as in how I've tried to frame the docs): 1.
emit(value)
call makes the value emitted 2.
onEach { value -> ... }
makes the value received
m
Ah I see.
.lastReceivedValue
then
e
Cannot have
.lastReceivedValue
with multiple subscribers. They might be receiving at a different rate.
m
Even if all collect directly from the
StateFlow
?
I think that would make it not suitable for React anyway because you’d never have a consistent state across all components.
e
Does not matter direct or not. It is all asynchronous. Subject to their dispatchers and their queues.
If you only render, don't do derived value, then it should not be a problem.
m
How so? If components use
.value
as state and collect for updates, then that’s out-of-sync already.
If I only collect then I cannot provide initial state. And it’s not consistently collected.
e
However, you might want to have some synchronous framework for your React needs. If you don't use asynchrony and all your subscribers are sync, then you might be getting more trouble from flows than benefits.
But it does not matter if it is "out of sync" if you only use that
useState
code you've shown. It is Ok that it will take some time to see new value and only then update itself.
If I'm just using
useState
here and there in my react components to render something based on a value, then the worst thing that can happen is that it'll wait for a few ticks (based on how JS queues are internally prioritized) before it renders an update.
m
I still have an inconsistency. Component A re-renders for an unrelated state change but picks up our
StateFlow
new value without being notified about it. Component B re-renders a little later because it got notified about
StateFlow
value change. So for a brief period A and B don’t represent the same value of the
StateFlow
.
e
It can indeed happen. But if that's only render no one notices. Such glitches only cause problems when you do some distructive action based on inconsistent state.
m
Yeah that was kind of my case. A: we need to redirect to sign-in B: user is signed in, user needs to be elsewhere It’s bad if that inconsistency is between parent (user signed out = we need to sign in) and child (sign in page -> user is signed in = we need to be elsewhere). I can likely work around that somehow. But that just happens way too easily.
I mean parent decides to show sign-in page b/c user not signed in. And child (sign-in page) decides to redirect forward because sign-in is not needed.
Anyway, for now it works and I’m aware of the issue. I’ll probably have to come up with a solution that’s more synchronous and doesn’t involve a
StateFlow
.
e
You can try a simple workaround. May help in your case
Try using “immediate dispatcher” for your subscribers. That’ll make it “mostly synchronous”
m
I’m using
GlobalScope
right now and there’s no
GlobalScope.immediate
. How do I use the immediate dispatcher?
e
I’d love to have a definite solution, but I don’t have one. In my experience all event systems (even synchronous) are prone to such glitches as soon as dependent values of any forms (even implicit dependent values as in your case) start to appear
Use
Dispatchers.Main.immediate
m
I’ll give it a try, thanks. And thank you for your thoughts and feedback. I’ll keep an eye open for more scenarios and then re-think the current approach.