我自己認爲是有以下兩個理由
一樣以 Todo list 為例子,假設我今天我想將 Todo 的資料帶去另外一個 component 而且todo-list/+page.svelte
這個頁面裡有 invoke 的 component,那這時我們就不能將 todoList
這個 state 當作 props 直接傳過去,因為這個 state 只存在在這個頁面裡。
當然這種例子也許讓
todoList
是從 server 撈來的,然後我在每個 component / 頁面分別去 query 就可以解決了。
那假設今天我們是想把 addTodo
這個控制 todo 新增邏輯的 function 給傳給一個中間經過好幾層封裝的 component 像是某個 dialog 中的某個 button 的 onclick ,那這時我們一路往下傳當然是可以的,只是維護起來會有點麻煩而已,特別是在要把改變 state 的 function 及 state 本身往下傳時就會覺得有那麼一點醜。
雖然說「改變 state 的 function」這件事情在 Svelte 不是必要的,只要是 bindable 的話,理論上就算在很底層的 children 也是能直接更新上面傳下來的 state 。但只要是稍微複雜的更新邏輯我會建議封裝成 function 再往下傳會比較好。
Svelte 本身提供了一個非常方便的 global state 的實現方式,就是建立一個 .svelte.[jt]s
檔裡面就可以使用 rune 了,也就代表我們能在 Svelte component 的範疇外使用了 $state
及 $derived
了,所以我們就能非常簡單地建立一個不在 component 裡但具有 reactive 的變數了。當然還是有一些限制像是沒辦法使用 $effect
,畢竟他不在 component 裡沒有所謂的掛載 DOM 的事情。
// in todo.svelte.ts
export interface Todo {
id: number;
title: string;
content: string;
done: boolean;
}
const todoListStoreCreator = () => {
let todoList = $state([
{
id: Date.now(),
title: 'First todo',
content: 'This is the first todo',
done: true
},
{
id: Date.now() + 1,
title: 'Second todo',
content: 'This is the second todo',
done: false
},
{
id: Date.now() + 2,
title: 'Third todo',
content: 'This is the third todo',
done: false
}
]);
const addTodo = (todo: Pick<Todo, 'title' | 'content'>) => {
todoList.push({
id: Date.now(),
done: false,
...todo
});
};
const removeTodo = (id: number) => {
todoList = todoList.filter((todo) => todo.id !== id);
};
const toggleDone = (id: number) => {
todoList = todoList.map((todo) => {
if (todo.id === id) {
todo.done = !todo.done;
}
return todo;
});
};
return {
get todoList() {
return todoList;
},
addTodo,
removeTodo,
toggleDone
};
};
const todoListStore = todoListStoreCreator();
export default todoListStore;
會發現跟在寫 ts 一樣,只是我們可以使用 rune,然後需要小小注意的一點是,get todoList()
這裏你必須要用類似這種 getter 形式來寫,不使用 get
也可以但總之不能直接回傳 {todoList,addTodo}
這種形式,因為這樣子會導致使用 todoListStore.todoList
時只會拿到 todoListStoreCreator()
當下 todoList
的值。
至於詳細的解釋,可以參考 Svelte 的作者 Rich Harris 的介紹。
BTW 這也是為什麼我喜歡 Svelte 的其中一個理由, Rich 真的很常出來說明為什麼它們要這麼做。
使用上會像是這樣
<script lang="ts">
import { fly } from 'svelte/transition';
import Input from './Input.svelte';
import PageLayout from './PageLayout.svelte';
import todoListStore, { type Todo } from './todo.svelte';
let title = $state('');
let content = $state('');
</script>
<PageLayout>
<div class="grid grid-cols-2 mb-8 gap-x-8">
<Input label="Title" bind:value={title} maxLength={20} />
<Input label="Content" bind:value={content} />
</div>
<button
class="btn btn-primary"
onclick={() => {
todoListStore.addTodo({ title, content });
}}
>
Add Todo
</button>
<div class="divider"></div>
{#each todoListStore.todoList as todo (todo.id)}
{@render card(todo)}
{/each}
</PageLayout>
會發現原本那些新增及移除的 function 就可以從這個頁面中拿開,也就代表如果今天想要在其他 component 中使用 todoListStore
相關的操作都是沒問題的。
某種程度上我覺得這算一種「UI與商業邏輯的解耦」當然最後的最後一定會有一個 UI 必須去串接商業邏輯相關的操作,但至少這種方式可以讓我們把耦合的範圍限縮在特定地方就好。
https://github.com/toddLiao469469/30days-for-svelte5/tree/day12/src/routes/todo-list