[S4SRD]S01E03 - Context Evolved (Updatable Context in Svelte)

tiagobnobrega

Tiago Nobrega

Posted on July 14, 2019

[S4SRD]S01E03 - Context Evolved (Updatable Context in Svelte)

This is a series about applying some common React concepts and patterns in sveltejs. This season is about hooks. Last episode We've set the objective of exploring hooks and how to implement some of its features in svelte (Check it out if You haven't so far). In this episode, I'll show how to update a value in svelte context. Ready your popcorn and welcome to:

🙃

Svelte For The Stubborn React Developer

Abstract

Last episode we created a hook to access context. Now we are looking into how to use context in a way we can update its value.

The problem emerged from a statement about getContext and setContext functions:

The catch here is that You need to call them during component initialization, so don't call them inside onMount, onDestroy, clickEvents, etc.

I asked a similar question in stack overflow and @Rich_Harris was kind enough to point me in the right direction. Instead of just laying out the answer I decided to walk through the concept that would culminate in this idea. This way We get a better understanding of why instead of just focusing on how. Of course, if you don't want to journey this, just read the TL;DR 😉.

TL;DR

Since the reference to a context value can't be updated. We need a way to access an updatable value in context. Svelte stores are perfect for this because they can be updated and observed. So basically, just use context with a store as its value.

Can't Update The Reference, Now What ?!?

Let's start with our goal. We want to be able to define a context value, and then update this value, and finally react to this and use the new value. But... We can't update the context value reference after component initialization.

Think of our context value as a const. In javascript we can't update the const reference, right?

(()=>{
    const a = {value:'initial'};
    a = {value: 'updated'} // ⭠ TypeError: Assignment to constant variable.
    console.log(a);
})()
Enter fullscreen mode Exit fullscreen mode

But, if we have an object assigned to a const we can update any value (mutate) in it:

(()=>{
    const a = {value:'initial'};
    a.value = 'updated'
    console.log(a); // outputs: {value: "updated"}
})()
Enter fullscreen mode Exit fullscreen mode

Isn't This Episode About svelte ??

Ok... How we apply this concept in svelte's context (I mean 🤔... svelte context context 😕... You got it!). Try to follow along with the comments in this nonpractical example:

<!-- App.svelte -->
<script>
    import ContextValue from './ContextValue.svelte';
    import {setContext, getContext} from 'svelte';
    setContext('value',{value:'inital'}); // ⭠ Create context
</script>
<ContextValue /> <!-- Import component that use the context -->

<!-- ContextValue.svelte -->
<script>
    import {getContext} from 'svelte';
    const contextValue = getContext('value'); // ⭠ Get context.

    function logContextValue(){ //⭠ Function to log current context value
        console.log(contextValue)
    }

    function updateContext(){ // ⭠ Function to "update" context
        myContext.value = 'updated'
    }
</script>
<button on:click={updateContext} >Update Context</button> <!-- ⭠ "Updates" context -->
<button on:click={logContextValue}>Log Context Value</button> <!-- ⭠ Log context -->
Enter fullscreen mode Exit fullscreen mode

The expected idea is to:
1 - click "Log Context Value" button ⮕ outputs initial value

2 - click "Update Context" button;

3 - click "Log Context Value" button ⮕ outputs updated value

And... It works!

result-1

Still Messy

Yeah... Not so great yet. The logic is all over the place, and we didn't even create a reusable function for that (imagine using it in many components). We need several functions to make it work. It's messy. How about this?

//smartContext.js
import {setContext, getContext} from 'svelte';

export function setSmartContext(contextObject){
    setContext('value',contextObject);
}

export function getSmartContext(){
    const ctx = getContext('value');
    return {
        get:()=>ctx,
        update: newValue => ctx.value = newValue
    }
}
Enter fullscreen mode Exit fullscreen mode

Better... It's isolated in one module. We could use it like such:

<!-- App.svelte -->
<script>
    import ContextValue from './ContextValue.svelte';
    import {setSmartContext} from './smartContext.js'
    setSmartContext({value:'inital'}); //⭠ Set a smartContext
</script>
<ContextValue />

<!-- ContextValue.svelte -->
<script>
    import {getSmartContext} from './smartContext.js';
        const smartContext = getSmartContext('value'); //⭠ get a smartContext
        function updateContext(){
            smartContext.update('updated') //⭠ updates smartContext
        }
        function logContextValue(){
            console.log(smartContext.get()) //⭠ Set smartContext value
        }
</script>
<button on:click={updateContext} >Update Context</button>
<button on:click={logContextValue}>Log Context Value</button>
Enter fullscreen mode Exit fullscreen mode

Still... It only works for a single value. If we want 2 distinct context values, we would need to replicate our smartContext.js (not so smart...).

Making It More Reusable

Actually, if You're creative enough You could realize the smartContext is just an object that updates a variable in its scope (or context). For that, it doesn't even need an external context if there's an internal context (or scope). It turns out there's a great feature in javascript for this: Functions !!!! Look:

//smartContext.js
export default (defaultValue)=>{
        let value = defaultValue; //⭠ scope value
        return {
            set: newValue=>{
                value=newValue //⭠ update scope value
            },
            get: ()=>value,//⭠ get scope value
        };
    };
Enter fullscreen mode Exit fullscreen mode

Interesting... But this doesn't bring to the table all features a svelte context has to offer. So, let's combine them and create 2 smartContexts.

<!-- App.svelte -->
<script>
    import ContextValue from './ContextValue.svelte';
    import {setContext} from 'svelte' //⭠ import default svelte context
    import smartContext from './smartContext.js' // ⭠ import smartContext "builder"

    //⮦Set a context value to a smartContext
    setContext('value', smartContext('initial')) 
    //⮦Set another context value to a smartContext
    setContext('unused', smartContext('unused'))
</script>
<ContextValue />

<!-- ContextValue.svelte -->
<script>
      import {getContext} from 'svelte';
      const smartContext = getContext('value'); //⭠ get a smartContext
      const getUnusedContext = getContext('unused');//⭠ get a smartContext
      function updateContext(){
        smartContext.update('updated')//⭠ update the smartContext
      }
      function logContextValue(){
        console.log(smartContext.get())//⭠ get the smartContext value
      }
</script>
<button on:click={updateContext} >Update Context</button>
<button on:click={logContextValue}>Log Context Value</button>
Enter fullscreen mode Exit fullscreen mode

Adding Reactiveness

That's a lot better now! And I know It might seem like a great round trip to get to the same place, but It's important to understand and split the concepts. Bear with me just a bit. So are we done? Not really. We need:

to be able to define a context value, and then update this value, and finally react to this and use the new value

We are already defining a context value and updating this value but we are not reacting to this update. The only way to get the updated value so far is by executing an imperative action (hence, "click the button"). If we had this value displayed on ContextValue.svelte, it wouldn't be automatically updated. Let's try that:

<!-- ContextValue.svelte -->
<script>
      import {getContext} from 'svelte';
      const smartContext = getContext('value'); //⭠ get a smartContext
      const getUnusedContext = getContext('unused');//⭠ get a smartContext
      function updateContext(){
        smartContext.update('updated')//⭠ update the smartContext
      }
      function logContextValue(){
        console.log(smartContext.get())//⭠ get the smartContext value
      }
</script>
<button on:click={updateContext} >Update Context</button>
<button on:click={logContextValue}>Log Context Value</button>
Enter fullscreen mode Exit fullscreen mode

And the result is:

result-2

A Better SmartContext

The value is not automatically updated. It makes sense, why would it anyway? We need a way to obverse or to subscribe to this value updates. Before jumping to address this, let's consolidate what we need:

A way to store, update, a subscribe to a scoped value.

The scope, as we've seen, it's handled by svelte context using getContext and setContext. Our smartContext already stores and updates the value, but isn't observable. svelte comes with a handy feature to help us out: svelte store.

Stores in svelte do exactly that, so we can completely replace smartContext with it. First App.svelte

<!-- App.svelte -->
<script>
    import ContextValue from './ContextValue.svelte';
    import {setContext} from 'svelte'; //⭠ import svelt context
    import { writable } from 'svelte/store'; //⭠ import svelt writable store

    let smartContext = writable('initial');//⭠ initialize store
    setContext('value',smartContext);//⭠ set context value as the store
</script>
<ContextValue />

Enter fullscreen mode Exit fullscreen mode

At this point, we will observe to store updates and reacts to it by updating a component variable. It's a little different than the previous approach of accessing the store value. When the store value changes, so our variable value will.

<!-- ContextValue.svelte -->
<script>
      import {getContext,onMount} from 'svelte';
        //⮦ get svelt store(replaced our smartContext)
        let smartContext = getContext('value'); 
        let contextValue;//⭠ this variable will hold the store value (context value)
        //⮦ update our variable whenever the store value get updated
        onMount(()=>smartContext.subscribe(v=>contextValue = v))

        //⮦ Function to update store value
        function updateContext(){
            smartContext.update(()=>'updated')
        }
       //⮦ We don't need to access store value, just access our "synced" variable
        function logContextValue(){ 
            console.log(contextValue)
        }
</script>
<h1>{contextValue}</h1> <!-- print out our variable value -->
<button on:click={updateContext} >Update Context</button>
<button on:click={logContextValue}>Log Context Value</button>
Enter fullscreen mode Exit fullscreen mode

And the result:

result-3

There You go. Now we're talking!!

Making it even better... Get me some sugar !

It works! Finally. Still too verbose though, don't You think? Stores, as a built-in feature of svelte comes with a syntax sugar we can use: auto-subscriptions. It works by just putting a dollar sign ($) before your store variable name. Simple as that! We just need to change our ContextValue.svelte component. Check it out:

<!-- ContextValue.svelte -->
<script>
      import {getContext,onMount} from 'svelte';
        let smartContext = getContext('value');
        function updateContext(){
            smartContext.update(()=>'updated')
        }
        function logContextValue(){ 
            console.log($smartContext) //⭠ auto-subscribed value
        }
</script>
<h1>{$smartContext}</h1> <!-- //⭠ auto-subscribed value -->
<button on:click={updateContext} >Update Context</button>
<button on:click={logContextValue}>Log Context Value</button>
Enter fullscreen mode Exit fullscreen mode

Now It's smaller and more concise. And We get the added bonus of having svelte unsubscribe from the store when the component gets destroyed. One small problem with the previous version of the code I omitted.

Things are starting to get interesting. I recommend taking a look at stores examples(https://svelte.dev/examples#writable-stores) and documentation(https://svelte.dev/docs#writable) from svelte official docs. It's extremely simple to use.

I might add an episode or two on the subject. Who knows? Let me know if You think I'ts interesting!!

❕⚠️⚠️⚠️ Spoiler Alert ⚠️⚠️⚠️❕
I promise I'll get to HOC. Just a couple more things first!

❕⚠️⚠️⚠️ Spoiler Alert ⚠️⚠️⚠️❕

💖 💪 🙅 🚩
tiagobnobrega
Tiago Nobrega

Posted on July 14, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related