DEV Community

Cover image for [S4SRD]S01E03 - Context Evolved (Updatable Context in Svelte)
Tiago Nobrega
Tiago Nobrega

Posted on

[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);
})()

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"}
})()

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 -->

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
    }
}

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>

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
        };
    };

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>

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>

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 />

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>

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>

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 тЪая╕ПтЪая╕ПтЪая╕ПтЭХ

Top comments (0)