Hi, I’m subscribing to Flow objects in my React Co...
# javascript
l
Hi, I’m subscribing to Flow objects in my React Components to update state. I’m using a singleton instance of
MainScope
to launch a new coroutine in which I collect the Flow. If I leave things like that and the component gets unmounted because the user for example logs out, then the coroutine keeps running and collecting from the flow after the component is unmounted, resulting in React errors/warnings. I solved it by assigning a Job and then cancelling it in
componentWillUnmount
. This doesn’t feel right though and I’d prefer some better solution in case anyone has suggestions. I was thinking something similar as Android view models have a
viewModelScope
that automatically gets cancelled when the view model is cleared, but I can’t seem to find a way to do this.
t
React hooks (
useEffect
for example) contains
cleanup
logic. In my case I cancel
Job
which was started by hook
l
Thanks @turansky, I just read the documentation on it. Do you have any code sample that you could share by any chance?
I guess something like this:
Copy code
val users = functionalComponent<UsersProps> { props ->
    val (viewModelOutput, setViewModelOutput) = useState<UsersViewModel.Output?>(null)

    useEffectWithCleanup(emptyList()) {
        val job = mainScope.launch {
            props.viewModel.output.collect { output ->
                setViewModelOutput(output)
            }
        }

        return@useEffectWithCleanup {
            job.cancel()
        }
    }
which was the following before:
Copy code
class UsersComponent: RComponent<UsersProps, UsersState>() {

    override fun componentDidMount() {
        job = mainScope.launch {
            props.viewModel.output.collect { output ->
                setState {
                    this.viewModelOutput = output
                }
            }
        }
    }

    override fun componentWillUnmount() {
        job?.cancel()
        job = null
    }
t
Users.
l
tnx. And why use GlobalScope instead of MainScope?
t
It’s open question for me (research required) Current arguments: 1. React kill jobs automatically (do context work) 2. If context isn’t important, let’s use simplest (
GlobalScope
) 3. I don’t see
MainScope
benefits in JS
l
I’m coming from an iOS background myself so I’m not too familiar yet with coroutines and not completely sure what the difference is between MainScope and GlobalScope, but from what I’ve read, you always need to keep track of coroutines created by GlobalScope yourself (like is done in your example), otherwise it gets cancelled right? Also in the guide at https://play.kotlinlang.org/hands-on/Building%20Web%20Applications%20with%20React%20and%20Kotlin%20JS/08_Using_an_External_REST_API they use MainScope.
e
On JS there's no difference between GlobalScope and MainScope. It matters on Android, though.
🙂 3
t
@elizarov Does it mean,
GlobalScope
is more preferable on JS, when created `Job`s cancelled automatically by another library (
react
in my case)?
e
If you cancel them yourself, then there's no actual difference on JS. However, for consistency I'd recommend following the practice of using
MainScope
for all kinds "UI actors" (that is, the things that directly work with UI) and
GlobalScope
for all kind of "background actors" that performs any kind of business logic. Again, on JS it is all irrelevant (any heavy code will lock your UI anyway), but still keeping distinction in mind might be helpful.
😀 2
s
With GlobalScope being marked as sensitive, what’s the right way to work in Javascript? In a simple example, if I want to make an http call with ktor in reaction to a click, what context would I use? I don’t want to block the main ui obviously.
👀 1
a
Also curious about ^^ Was originally going to use GlobalScope for that kind of stuff, but the Delicate API warning has be second guessing myself
t
cc @elizarov
e
It depends on what UI framework you use in your JS app and how do you manage lifecycle there. The overall integration approach is explained here https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/#custom-usage
t
GlobalScope
is legal in
react
case (with hooks). Do I need custom global scope in that case or
languageSettings
configuration will be fine?
e
No. React components have a lifecycle (which you can subscribe to by overriding methods in classes or by using hooks in functional components). You should be cancelling all the asynchronous work that your react component had spawned when the component is unmounted.
t
Job
cancel works fine in that case
Copy code
val user: User? by useState(null)

useEffect(id) {
    val job = GlobalScope.launch {
        user = Api.getUser(id)
    }

    // cleanup
    // on `id` change and on unmount
    job::cancel 
}
e
I’d rather create a reusable abstraction that gives me a life-time-bounded scope (similar to Compose’s rememberCoroutineScope) and then use it launch coroutines. Seems cleaner to me.
t
Api method can be called from custom hook (separate function) and component will be unknown in that case
similar to Compose’s rememberCoroutineScope
Is it component scope or application scope?
s
So if I’m understanding.. you would pass a
Job
to all React components, and then within each one, create a scope with a new job… and when the component is unmounted, call and cancel the component’s job. Is there ANY examples of coroutines with React? Everything I’ve ever seen uses GlobalScope (including many official Jetbrains sources)
Would love to see a
Best Practices for Coroutines in the Browser
For now, I’m continuing to use GlobalScope because: 1) it’s working just fine. and 2) no other methods are documented at all for JS, let alone React.
👍 2
t
For remote calls :)
Copy code
inline fun useAsyncEffect(
    vararg dependencies: dynamic,
    suspend effect: () -> Unit,
) {
    useEffect(dependencies) {
        val job = GlobalScope.launch(effect)
        // cleanup
        job::cancel
    }
}