iT邦幫忙

2024 iThome 鐵人賽

DAY 12
0
Modern Web

Svelte 的奇妙冒險系列 第 12

[Svelte 的奇妙冒險] Day 12 - global state management

  • 分享至 

  • xImage
  •  

為什麼需要 global state management

我自己認爲是有以下兩個理由

跨組件共用 state

一樣以 Todo list 為例子,假設我今天我想將 Todo 的資料帶去另外一個 component 而且todo-list/+page.svelte 這個頁面裡有 invoke 的 component,那這時我們就不能將 todoList 這個 state 當作 props 直接傳過去,因為這個 state 只存在在這個頁面裡。

當然這種例子也許讓 todoList 是從 server 撈來的,然後我在每個 component / 頁面分別去 query 就可以解決了。

避免 Props drilling

那假設今天我們是想把 addTodo 這個控制 todo 新增邏輯的 function 給傳給一個中間經過好幾層封裝的 component 像是某個 dialog 中的某個 button 的 onclick ,那這時我們一路往下傳當然是可以的,只是維護起來會有點麻煩而已,特別是在要把改變 state 的 function 及 state 本身往下傳時就會覺得有那麼一點醜。

雖然說「改變 state 的 function」這件事情在 Svelte 不是必要的,只要是 bindable 的話,理論上就算在很底層的 children 也是能直接更新上面傳下來的 state 。但只要是稍微複雜的更新邏輯我會建議封裝成 function 再往下傳會比較好。

.svelte.ts (or .js)檔

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 必須去串接商業邏輯相關的操作,但至少這種方式可以讓我們把耦合的範圍限縮在特定地方就好。


參考資料

source code

https://github.com/toddLiao469469/30days-for-svelte5/tree/day12/src/routes/todo-list


上一篇
[Svelte 的奇妙冒險] Day 11 - motion 與 transition
下一篇
[Svelte 的奇妙冒險] Day 13 - runed,一個好用的 utility library
系列文
Svelte 的奇妙冒險30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言