Svelte Context 相關

tags: Svelte
category: Front-End
description: Svelte Context 相關
created_at: 2023/04/18 18:00:00

cover image


回到 手把手開始寫 Svelte


前言

這個在 Vue 印象中是沒有直接對應的東西,但在 ReactReact Context,但因為我也很久沒用,細節也差不多忘了,但我只確定在 SvelteContext 使用起來比較簡單(?)


使用之前你要知道的

除了語法以外,有一點很重要,就是 context 只能使用在最頂層,也就是你的組件被初始化的時候用。


什麼是 Context

這是一種組件之間傳遞值的方式,跟 Store 不同,Store可以任意去取得他,但 Context 只能從上而下傳。

Q: 從上而下傳的話那麼跟 Props 又有什麼不一樣?

A: Props 必須每一層都放,而 Context 可以直接取用,只要上層曾經有定義過,下層就可以直接拿。

但這邊要注意的是,Store 具有響應式Reactive,而 Context 沒有,所以如果你希望 Context 一樣具有 Reactive,那麼你可以把 Store 塞進 Context

在某些時刻 Context 會很有用,因為他會優先取到離自己最近的 Context,這待會會有範例。


基本使用

假設接下來都用兩個組件做到三層來做範例。(Outer 包住 Inner)

/src/routes/+page.svelte: 設定 Context

<script lang="ts">
    import { setContext } from 'svelte';
    import Outer from '../components/Outer.svelte';

    let value = 100;
    setContext('value', value);
</script>

<h1>App</h1>
<Outer />

/src/components/Outer.svelte: 什麼事都不做,只渲染 Inner

<script>
    import Inner from './Inner.svelte';
</script>

<Inner />

/src/components/Inner.svelte: 拿到 Context 的值並渲染出來

<script lang="ts">
    import { getContext } from 'svelte';

    const value = getContext('value');
</script>

<div>Inner: {value}</div>

上面有提到說 Context 不具有 Reactive,所以可以嘗試把最上層改變一下:

<h1>App: {value}</h1>
<button on:click={() => value++}>Increment</button>
<Outer />

會發現說你按下按鈕,只有當前的組件 value 有增加,而後面吃到的 Context 並沒有跟著更新。

這時如果你需要他更新,就需要整合 StoreContext 當中,如下:

let value = writable(100);
setContext('value', value);

而這時因為 value 不再是純值,使用的地方必須要加上 $ 幫忙處理。

<h1>App: {$value}</h1>
<button on:click={() => $value++}>Increment</button>

然後使用的地方(Inner)也需要做修改:

<script lang="ts">
    import { getContext } from 'svelte';
    import type { Writable } from 'svelte/store';

    const value = getContext<Writable<number>>('value');
</script>

<div>Inner: {$value}</div>

這時再到畫面上戳按鈕,兩邊的 value 就會同步了。


Context 存放 DOM

因為 Context 只能放在最頂層,所以也沒辦法被掛在 onMount 之類的生命週期中,那在頂層根本還沒被渲染,所以你肯定抓不到的。

那這邊提供兩個做法,一個是照上面的方式,把它改成 Store 的方式,第二種方式是把 get 函數放到 Context 用。

第一種方法因為跟上面差不多,就不給範例了。

這邊提供第二種方式的範例,在 Context 當中塞一個函數。

<script lang="ts">
    import { setContext } from 'svelte';
    import Outer from '../components/Outer.svelte';

    let app: HTMLElement;
    setContext('app', {
        getApp() {
            return app;
        }
    });
</script>

<h1 bind:this={app}>App</h1>
<Outer />

然後可以再 Inner 當中這樣使用:

<script lang="ts">
    import { getContext, onMount } from 'svelte';

    interface AppContext {
        getApp(): HTMLElement;
    }

    const app = getContext<AppContext>('app');

    console.log('init: ', app.getApp());

    onMount(() => {
        console.log('onMount: ', app.getApp());
    });
</script>

<div>Inner</div>

這邊把 interface 定義在一起,實際上你要定義 Context 可以像是 Store 一樣開獨立檔案去定義。

然後可以注意到 console,在 init 的時候會抓不到,只有在 onMount 之後才抓的到你要的東西。


Contextkey

上面都是把 Contextkey 直接用字串值寫死,這是因為在練習所以也寫在同個檔案。

實際上你可能會有很多 Context,或是別的套件有使用 Context,你都用字串值有可能會跟別人的名字衝突,就會導致出問題。

因為上面也有提到說他會抓近的,所以可以看看下面的範例:( Outer 終於有事做了)

+page.svelte

<script lang="ts">
    import { setContext } from 'svelte';
    import Outer from '../components/Outer.svelte';

    setContext('value', 'App');
</script>

<h1>App</h1>
<Outer />

Outer.svelte

<script>
    import { setContext } from 'svelte';
    import Inner from './Inner.svelte';

    setContext('value', 'Outer');
</script>

<Inner />

Inner.svelte

<script lang="ts">
    import { getContext } from 'svelte';

    const value = getContext('value');
</script>

<div>Inner value: {value}</div>

然後你會看到 Inner 印出來的是抓到 Outer 給的值,而不是最上層。

然後你也可以在 Outer 做一個實驗:

<script>
    import { getContext, setContext } from 'svelte';
    import Inner from './Inner.svelte';

    const valueFromApp = getContext('value');
    setContext('value', 'Outer');
    const valueFromSelf = getContext('value');
</script>

<div>Outer: {valueFromApp}, {valueFromSelf}</div>
<Inner />

你會看到他呈現出: Outer: App, Outer

要避免這個問題,你可以使用 ES6 提供的 Symbol

簡單來說 Symbol 就是獨一無二的東西,你建立出來的 Symbol 不會等於其他 Symbol,舉例來說

const a = Symbol()
const b = Symbol()
console.log(a == b, a != b)  // false true

// or

const a = Symbol('test')
const b = Symbol('test')
console.log(a == b, a != b) // false true

所以你可以把這個 Symbol 當成 Contextkey,這樣他就永遠不會衝突了,但也要記得把 key export 出來給別人用,不然沒人拿的到XD


Context 的另一種用途

最前面有提到說某些時候他很有用(?),因為他抓近的,所以你可以用程式去處理做出一種遞迴(recursive)的感覺。

首先 +page.svelte

<script lang="ts">
    import { setContext } from 'svelte';
    import Counter from '../components/Counter.svelte';

    setContext('value', 10);
</script>

<h1>App</h1>

<Counter />

然後 Counter.svelte

<script lang="ts">
    import { getContext, setContext } from 'svelte';

    const value: number = getContext('value') || 0;
    setContext('value', value - 1);
</script>

<div>Count: {value}</div>

{#if value > 0}
    <svelte:self />
{/if}

svelte:self 這是 Svelte 提供的特殊的元素,會在下一篇介紹,這裡用到的 self 你可以把他想成就是再掛一次 <Counter />,就是再掛一次自己當前的組件這樣。

雖然上面這個範例單純用 props 也可以達到,但只是想帶出 Context 也可以這樣做。雖然好像很沒必要

相信他應該多少還是有點用處的,只是我現在暫時想不到用途XD


Context 相關函數

Context 除了 getset 以外,還有兩個:

  • hasContext: 判斷 Context 是否存在
  • getAllContexts: 取得當前組件所有可使用的 Context

你可以把上面的 Counter 套用 getAllContexts 試試看,會看到只有一個 value,因為前面有提過後面會把前面的蓋掉(也就是只取到離自己最近的)。


總結

  • Context 只能使用在最頂層(元件初始化的時候)
  • Context 不具有 reactive,如果要,可以和 Store 結合使用



最後更新時間: 2023年04月18日.