Fred Hors
Fred Hors

Reputation: 4156

Svelte store conditional auto subscription

Reproduction steps:

Expected:

I thought that the $store can subscribe (with the $:) only if canSubscribe is true.

The question is: Why $store subscribe if canSubscribe is false?

Am I wrong?

Upvotes: 2

Views: 2586

Answers (3)

rixo
rixo

Reputation: 25041

Am I wrong?

Yes. Don't blame yourself though, your expectation seems only logical to me. But that's not the way it works.

As a general rule, if there is a $ prefixed variable somewhere in your component's code, then it must be a store, and it will be subscribed right away on component creation, and unsubscribed when the component is destroyed.

A minor exception to this rule was introduced quite recently though (with this PR). I'll let you follow the trail down the rabbit hole if you want to know the whole discussion. The crux of it is that, now, a store subscription must be a store or nullish (that is, null or undefined -- see this comment).

This means it is now possible to hack your way into the behavior you expected, if needed. We're coming back to this.

Why $store subscribe if canSubscribe is false?

Because stores are subscribed right away. My understanding, from the discussion in the issues linked above, is that it is for performance (byte size) and sanity (fail fast and visibly if someone tries to subscribe to something that is not a store). Makes sense to me.

Now, back to the question you didn't ask: how to subscribe only when/if needed? Put the store in the auto subscribed variable only when needed, and keep it nullish otherwise.

Don't do:

$: started && $store

Do this instead:

$: proxyStore = started ? store : null
$: console.log($proxyStore)

Full example (REPL):

<script>
    import { writable } from 'svelte/store'

    const state1 = { subscribed: 0, unsubscribed: 0 }

    const store1 = writable(42, () => {
        state1.subscribed++
        return () => {
            state1.unsubscribed++
        }
    })

    const state2 = { subscribed: 0, unsubscribed: 0 }

    const store2 = writable(43, () => {
        state2.subscribed++
        return () => {
            state2.unsubscribed++
        }
    })

    let started = false

    $: started && $store1

    $: targetStore = started ? store2 : null

    $: $targetStore
</script>

<pre>
started = {started}
store1 = {$store1} {JSON.stringify(state1)}
store2 = {$targetStore} {JSON.stringify(state2)}
</pre>

<button on:click={() => {started = !started}}>
    {started ? 'Start' : 'Stop'}
</button>

Upvotes: 3

joshnuss
joshnuss

Reputation: 2047

Svelte walks the AST at compile time to determine auto-subscriptions.

It sets up a subscription even when the code accessing the store is unreachable.

For example:

import {foo} from './stores'

let condition = false

if (condition) { $foo }

Even though $foo is technically unreachable, that won't be known until runtime.

Alternative: You can always use a manual subscription to work around this. Example:

import {onDestroy} from 'svelte'
import {myStore} from './stores.js'

// subscribe when component is created
const unsubscribe = myStore.subscribe(value => {
  // this is called anytime the value of myStore changes
})

// make sure to unsubscribe when component is unmounted
onDestroy(unsubscribe)

Upvotes: 4

Stephane Vanraes
Stephane Vanraes

Reputation: 16451

In the reactive statement $: canSubscribe && $store you have an expression to be evaluated. Since it is reactive, Svelte has to determine when to re-evaluate this expression and this will happen on two occassions: when canSubscribe changes, or when $store changes. So it will have to make a subscribtion to both of those values, hence you see that you immediately get a subscriber in your code.

Note that we often do canDoSomething && canDoSomething() in JavaScript but that is not 100% the same as doing if (canDoSomething) { canDoSomething()} it just has, in most cases, the same effect.

Upvotes: 3

Related Questions