Svelte Context 相關
tags: Svelte
category: Front-End
description: Svelte Context 相關
created_at: 2023/04/18 18:00:00
前言
這個在 Vue
印象中是沒有直接對應的東西,但在 React
有 React Context
,但因為我也很久沒用,細節也差不多忘了,但我只確定在 Svelte
中 Context
使用起來比較簡單(?)
使用之前你要知道的
除了語法以外,有一點很重要,就是 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
並沒有跟著更新。
這時如果你需要他更新,就需要整合 Store
到 Context
當中,如下:
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
之後才抓的到你要的東西。
Context
的 key
上面都是把 Context
的 key
直接用字串值寫死,這是因為在練習所以也寫在同個檔案。
實際上你可能會有很多 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
當成 Context
的 key
,這樣他就永遠不會衝突了,但也要記得把 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
除了 get
、set
以外,還有兩個:
hasContext
: 判斷Context
是否存在getAllContexts
: 取得當前組件所有可使用的Context
你可以把上面的 Counter
套用 getAllContexts
試試看,會看到只有一個 value
,因為前面有提過後面會把前面的蓋掉(也就是只取到離自己最近的)。
總結
Context
只能使用在最頂層(元件初始化的時候)Context
不具有reactive
,如果要,可以和Store
結合使用