[S4SRD]S01E03 - Context Evolved (Updatable Context in Svelte)
Tiago Nobrega
Posted on July 14, 2019
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!
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:
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:
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 ⚠️⚠️⚠️❕
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
November 28, 2024