Eva's Dev Notes

前後端、AI 與日常開發筆記

0%

題目描述

You are given the heads of two sorted linked lists list1 and list2.

Merge the two lists into one sorted list. The list should be made by splicing together the nodes of the first two lists.

Return the head of the merged linked list.

給定兩個已排序鏈結串列的頭節點 list1 和 list2。
將兩個串列合併為一個已排序的串列。該串列應透過拼接前兩個串列的節點來建立。
回傳合併後鏈結串列的頭節點。

Example 1:
Input: list1 = [1,2,4], list2 = [1,3,4]
Output: [1,1,2,3,4,4]

Example 2:
Input: list1 = [], list2 = []
Output: []

Example 3:
Input: list1 = [], list2 = [0]
Output: [0]

解題思路

我一開始想法,會需要 2 迴圈去比較,然後組出一個新的 list node,最後再將新的 list node 回傳。
但是這樣的話,時間複雜度會是 O(n^2),因為每次都要從頭開始比較。
後來想說,因為兩個 list 都是已排序好的,所以可以用雙指標的方式,一次比較兩個 list 的頭節點,將較小的節點加入新的 list node,然後將該指標往後移動一位,這樣時間複雜度就會是 O(n),因為每個節點只會被比較一次。

範例程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} list1
* @param {ListNode} list2
* @return {ListNode}
*/
var mergeTwoLists = function(list1, list2) {
let head = null
let nowNode = null
let list1pointer = list1;
let list2pointer = list2;
// 兩兩比較
if(list1 === null && list2 === null) return head
if(list1 === null && list2 !== null) return list2
if(list1 !== null && list2 === null) return list1


// 決定 head
// 因為當你選擇了 list1pointer 作為頭節點後,你應該:
// 移動 list1pointer 指針:list1pointer = list1pointer.next
// 設定 nowNode 為當前的尾節點:nowNode = head
if(list1pointer.val <= list2pointer.val ){
head = list1pointer
list1pointer = list1pointer.next
}else{
head = list2pointer
list2pointer = list2pointer.next
}
nowNode = head

//進入迴圈檢查
while(list1pointer !== null && list2pointer !== null){
if(list1pointer.val <= list2pointer.val){
nowNode.next = list1pointer
list1pointer = list1pointer.next
}else {
nowNode.next = list2pointer
// 移動指針
list2pointer = list2pointer.next
}
nowNode = nowNode.next;
}
// 最後要再檢查。其中一不為 null
if(list1pointer !== null){
nowNode.next = list1pointer
}else{
nowNode.next = list2pointer
}

return head

};
閱讀全文 »

Given an array of meeting time intervals where intervals[i] = [startᵢ, endᵢ], determine if a person could attend all meetings.

給定一個會議時間 intervals 陣列,其中 intervals[i] = [startᵢ, endᵢ],判斷一個人是否能夠參加所有會議。

Example 1:

Input:

intervals = [[0,30],[5,10],[15,20]]

Output:

false

Example 2:

Input:

intervals = [[7,10],[2,4]]

Output:

true

Constraints:

  • 0 <= intervals.length <= 10⁴
  • intervals[i].length == 2
  • 0 <= startᵢ < endᵢ <= 10⁶

解題思路

要判斷一個人是否能夠參加所有會議,關鍵在於檢查會議時間是否有重疊。若有任何兩個會議的時間區間重疊,則無法參加所有會議。

重疊情況分析

以 [1,5] 和 [2,4] 為範例:

  1. 假設會議時間按開始時間排序:將所有會議按照開始時間由早到晚排列
  2. 檢查相鄰會議:排序後,只需檢查相鄰的會議是否重疊
  3. 重疊條件:前一個會議的結束時間 > 下一個會議的開始時間

步驟

  1. 將所有會議按開始時間排序
  2. 遍歷排序後的會議陣列
  3. 檢查每個會議的結束時間是否晚於下一個會議的開始時間
  4. 若發現重疊,回傳 false;否則回傳 true

這樣的方法時間複雜度為 O(n log n)(主要是排序的時間),空間複雜度為 O(1)。

閱讀全文 »

列表的動態排序和插入為常見的需求。然而,當面對大量資料時,傳統的「刪除重建」策略容易會成為效能瓶頸。本文將分享我在專案中如何透過 SeqNo 管理機制,將列表插入效能提升,同時保持資料一致性和系統穩定性。

在專案內有一下拉列表的功能,當使用者選擇某個選項後,會將該選項插入到列表中,並且會依據使用者的操作順序來進行排序。最初的實作方式是每次插入新選項時,都會重新計算整個列表的排序,這在資料量較大時,導致效能明顯下降。為了解決這個問題,引入了 SeqNo 管理機制。

原有問題

當使用者要在列表中間插入新的資料時,使系統面臨挑戰,並且在沒有加入 seqNo 管理機制前,容易遇到排序衝突的問題。

1
2
3
4
5
6
7
// 原有狀態
[
{ id: 'A', seqNo: 1 },
{ id: 'B', seqNo: 2 }, // 要在 B 後面插入新項目
{ id: 'C', seqNo: 3 },
{ id: 'D', seqNo: 4 }
]

如果直接將新項目設為 seqNo: 3,會與現有的項目 C 發生衝突。

傳統解決方案的困境

直觀的解決方案是重新排序所有項目:

1
2
3
4
5
6
7
8
9
10
// 刪除重建
async function insertListData(afterId: string,
newListData: ListData) {
// 1. 刪除所有後續項目 (N-M 次刪除)
await deleteListDataAfter(afterId);

// 2. 重新建立所有項目 (N-M+1 次建立)
await recreateListDataWithNewSeqNo(newListData,
subsequentListData);
}
  • 操作複雜度: O(2N+1) - 其中 N 為列表總長度
  • 資料庫壓力: 大量刪除和建立操作
  • 交易風險: 操作步驟過多,失敗機率高
  • 併發問題: 長時間鎖定,容易產生競爭條件

SeqNo 管理機制

只更新真正需要變動的項目,將複雜度進行改善,是插入點之後的項目數量。

  1. 排序基礎函式
    確保所有項目能依照 seqNo 正確排序。
1
2
3
4
5
6
7
8
// 排序工具
export function sortListDataBySeqNo(listData: ListData[]): ListData[] {
return [...listData].sort((a, b) => {
const aSeqNo = a.seqNo || 0;
const bSeqNo = b.seqNo || 0;
return aSeqNo - bSeqNo;
});
}
  1. 插入策略計算

只計算「插入點之後」需要調整的項目,並產生更新操作清單。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 export function calculateInsertStrategy(
existingListData: ListData[],
afterListDataId: string
): {
insertSeqNo: number;
affectedListData: ListData[];
updateOperations: Array<{ listDataId: string; newSeqNo: number }>;
} {
const sortedListData = sortListDataBySeqNo(existingListData);
const afterIndex = sortedListData.findIndex(data => data.id === afterListDataId);

if (afterIndex === -1) {
throw new Error('afterListDataId not found');
}

// 關鍵:插入點的 seqNo + 1
const insertSeqNo = sortedListData[afterIndex].seqNo! + 1;
// 只影響插入點之後的項目
const affectedListData = sortedListData.slice(afterIndex + 1);

// 產生更新操作清單
const updateOperations = affectedListData.map(data => ({
listDataId: data.id,
newSeqNo: data.seqNo! + 1
}));

return { insertSeqNo, affectedListData, updateOperations };
}

  1. 交易執行
    確保所有更新操作能在同一個 Transaction 中完成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
export async function executeSeqNoUpdates(
transaction: Transaction,
operations: Array<{ listDataId: string; newSeqNo: number }>
): Promise<void> {
for (const operation of operations) {
// 更新每個受影響的項目
const listDataRef = adminDb.collection('listData').doc(operation.listDataId);
transaction.update(listDataRef, {
seqNo: operation.newSeqNo,
updatedAt: new Date()
});
}
}

API 實現範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
  // POST /api/v1/listData
export async function POST(req: Request) {
// ... 驗證邏輯

if (afterListDataId) {
try {
// 步驟1: 計算最小更新策略
const { updateOperations, insertSeqNo } =
calculateInsertStrategy(existingListData, afterListDataId);

// 步驟2: Transaction 執行
const result = await adminDb.runTransaction(async (transaction) => {
await executeSeqNoUpdates(transaction, updateOperations);

// 插入新項目
const listDataRef = adminDb.collection('listData').doc();
transaction.set(listDataRef, {
...newListData,
seqNo: insertSeqNo
});

return { id: listDataRef.id, seqNo: insertSeqNo };
});

return NextResponse.json(result, { status: 201 });
} catch (error) {
if (error.message === 'afterListDataId not found') {
return NextResponse.json({ message: 'afterListDataId not found' }, { status: 404 });
}
throw error;
}
}
}

此專案的應用回顧與心得

這個問題最初是在「插入列表中間」時被發現的。
當時經常出現排序錯亂,追查後才發現是因為缺乏 SeqNo 管理機制,導致插入時 seqNo 發生衝突。

一開始的解法是 刪除重建,但這帶來兩個明顯的問題:
1.當資料量大時,對後端資料庫造成極大負擔。
2.使用者操作無法預測,若頻繁在列表中間插入或刪除,效能會快速下降。

引入 SeqNo 管理機制 後:

  • 插入效能明顯提升
  • 保持了資料一致性
  • 系統穩定性也大幅改善
    這樣的設計不僅解決了排序衝突,也大幅減少了資料庫的操作次數,讓整體效能更加穩定可

在開發 SaaS 或工具型應用時,「使用者驗證系統」往往是關鍵基礎設施。這篇文章將分享我如何使用 Next.js(App Router)+ MongoDB + NextAuth,同時支援 Google OAuth 和 自定義帳密登入,並分享專案 mongoDB 資料結構與 API 架構。

MongoDB 資料模型設計

為了支援多元登入方式,我設計了以下幾個集合(Collections):

  1. users - 使用者基本資訊
1
2
3
4
5
6
7
8
{
_id: ObjectId,
email: String,
name: String,
createdAt: Date,
updatedAt: Date
}

  1. authProviders - 自定義驗證方式(帳密)
閱讀全文 »

LeetCode 題目多樣,尤其是「二元樹(Binary Tree)」相關題型,容易讓人看了發愣。

本篇文章將教你如何釐清解法邏輯,並將二元樹題型拆解為兩大類解題策略,搭配範例與模板,幫助你用 DFS(深度優先搜尋)更系統地解題

類型一:遍歷型(Traversal Type)

這類題目需要「走過整棵樹的所有節點」,通常是為了計算某種總量,例如:

  • 節點總數
  • 最大深度
  • 所有路徑總和
  • 葉子節點的數量

解題思路

可根據需求使用:

  • 後序遍歷 + 回傳值 return → 適合需要組合子樹結果的情況
  • 前序遍歷 + 狀態變數追蹤 → 適合需要累加或記錄路徑狀態的情況

模板(後序 + return)

1
2
3
4
5
6
function dfs(node) {
if (node === null) return baseValue;
const left = dfs(node.left);
const right = dfs(node.right);
return combine(left, right, node);
}

模板(前序 + 狀態變數)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let result = 0;
let pathDepth = 0;

function traverse(node) {
if (node === null) return;

pathDepth++; // 遞迴深入,深度 +1
if (node.left === null && node.right === null) {
result = Math.max(result, pathDepth); // 若為葉子,更新最大深度
}

traverse(node.left);
traverse(node.right);

pathDepth--; // 回歸
}
閱讀全文 »

在前端開發中,動畫與互動是提升用戶體驗的關鍵元素。然而,當動畫與元件狀態、生命周期交織在一起時,實作上就可能遇到各種挑戰。

這篇文章記錄我在一個 React 專案中,解決「動畫重複觸發與重置」問題的過程,深入比較了使用 key 重置與 CSS Reflow(offsetWidth) 重置的方式

問題概述

在這個專案中,我有兩個自訂的元件,各自內部包含一個相同結構的 <input>。當使用者點擊某個操作後,我希望這些 input:

閱讀全文 »

練習來自 labuladong 的演算法筆記 的滑動視窗演算法核心程式碼模板。

基本設置:

用兩個指針來界定一個區間,也就是一個「窗口」:

  • 左指針 left:表示窗口的起始位置。
  • 右指針 right:表示窗口的尾部(通常採用左閉右開區間 [left, right))。

窗口內的資料會隨著 left 和 right 的移動而動態改變。解題的關鍵在於:

  • 擴大窗口:不斷將新的元素加入窗口,並更新窗口內的狀態或統計(例如字元出現次數)。
  • 檢查窗口是否滿足條件:根據題目要求檢查當前窗口是否為合法解。
  • 收縮窗口:如果當前窗口已滿足條件,嘗試從左側移除元素以找到更優(例如更短)的解,同時更新狀態。

模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var slidingWindow = function(s) {
// 使用合適的資料結構記錄視窗中的資料,根據具體題目變通。
// 例如:記錄字元出現的次數,或累加窗口內元素的和。
let window = {};

let left = 0, right = 0;
while (right < s.length) {
// 取出將要加入視窗的字元
let c = s[right];
right++; // 擴大窗口

// 更新窗口內資料
// 例如:window[c] = (window[c] || 0) + 1;
// 根據具體題目邏輯進行更新

// *** Debug 輸出位置 ***
// console.log("窗口: [%d, %d)", left, right);

// 當窗口不再滿足題目要求時,收縮窗口
while (/* 判斷窗口需要收縮的條件 */) {
// 取得將要移除的字元(為使變數名稱更具描述性可用 removedChar)
let removedChar = s[left];
left++; // 縮小窗口

// 更新移除字元對窗口資料的影響
// 例如:if (window[removedChar] ...) { ... }
}
}
};

題目

Given two strings s1 and s2, return true if s2 contains a permutation of s1, or false otherwise.

In other words, return true if one of s1’s permutations is the substring of s2.

給定兩個字串 s1 和 s2,若 s2 中包含 s1 的某個排列,則返回 true;否則返回 false。

換句話說,若 s1 的某個排列是 s2 的子字串,則返回 true。

Example 1:

Input: s1 = “ab”, s2 = “eidbaooo”
Output: true
Explanation: s2 contains one permutation of s1 (“ba”).

閱讀全文 »

這段時間在製作一個 snippet 管理的 side project,過程中遇到了一些跨頁面共享資料的需求。專案中有一包資料稱為 snippet,內部包含多個 folder 和小項目的 snippets。這些資料不僅會在 /snippet 路徑下的元件使用,還會跨 route 被其他頁面存取。

一開始,我選擇使用 React 的 Context API,透過建立一個 SnippetsContext 來管理狀態。在應用程式的最上層放置一次 SnippetsProvider,然後在需要的任何元件內直接使用 import { useSnippets } from ‘@/contexts/SnippetsContext’ 來取得 context。然而,隨著專案規模逐步擴展,這種方法逐漸顯露出其局限性。我發現即使已經有了最上層的 Provider,但在多個 dialog 或 component 中,仍然需要重複地導入 useSnippets,不僅造成代碼重複,也使得專案結構變得臃腫複雜。

為了解決上述問題,我嘗試了另一個狀態管理工具 —— Zustand。Zustand 提供了一種更直覺、更精簡的方式來建立和存取全域狀態,不需要透過 Provider 包裹元件,大幅減少了樣板代碼的使用,也讓資料流更加清晰易懂。

在這篇文章中,我會分享如何在專案中實際導入並使用 Zustand

初步實作 Zustand

1
npm install zustand

如果想使用持久化功能,也可以安裝:

1
npm install zustand/middleware

store 資料夾

在專案的 src 資料夾下建立一個 store 資料夾,這之中包含給 folder , snippet 的 slice。之所以採用 slice 是因為原本 snippet 的處理邏輯較為複雜,包含了 folder 的儲存、內部 snippet 的儲存,以及一些專門給 dialog 使用的 UI 狀態。透過將 store 分拆成多個 slice(例如 folderSlice 與 uiSlice),每個模組都只負責處理自己相關的 state 與操作,能更好地維護代碼的結構與可讀性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
src/
├── types/
│ └── snippet.ts // 共用型別,也可以有 auth.ts, settings.ts 等
└── stores/
├── snippet/
│ ├── slices/
│ │ ├── folderSlice.ts
│ │ ├── snippetSlice.ts
│ │ └── uiSlice.ts
│ └── index.ts // snippetStore.ts
├── auth/
│ └── index.ts // 假如之後, authStore.ts (可以進一步拆分 authSlices)
└── settings/
└── index.ts // 假如之後,settingsStore.ts

在專案中的 snippet 資料夾內,會建立一個 index.ts 檔案來整合所有獨立的 slice,並導出一個完整的 Zustand store。這麼做的優點是:之後在任何元件中需要使用狀態時,只需導入整個 store,而不需要逐一引入每個 slice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/stores/snippet/index.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { FolderSlice, createFolderSlice } from './slices/folderSlice';
import { SnippetSlice, createSnippetSlice } from './slices/snippetSlice';
import { UISlice, createUISlice } from './slices/uiSlice';

// 組合所有模組的型別
export type AppStore = FolderSlice & SnippetSlice & UISlice;

export const useSnippetStore = create<AppStore>()(
persist(
(set, get, api) => ({
...createFolderSlice(set, get, api),
...createSnippetSlice(set, get, api),
...createUISlice(set, get, api),
}),
{
name: 'my-snippets-storage',
// partialize 使我們只持久化 folders,UI 狀態不必存入 localStorage
partialize: (state) => ({ folders: state.folders }),
}
)
);
  • 使用 zustand 提供的 persist middleware,能輕鬆將特定狀態持久化到 localStorage。
  • 透過 partialize 參數,我能夠靈活地指定只有某些狀態 (例如:folders) 需要持久化,而 UI 或其他臨時狀態則保持記憶體內即可。

folderSlice.ts

在建立 folderSlice 時,透過 Zustand 的 StateCreator 定義了一個 slice,專門負責管理資料夾(folders)相關的狀態與邏輯。透過這個方式,能清楚地將資料夾的操作行為集中管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// src/stores/snippet/slices/folderSlice.ts
import { StateCreator } from 'zustand';
import { Folder } from '@/types/snippets';

export interface FolderSlice {
folders: Folder[];
setFolders: (folders: Folder[]) => void;
updateFolder: (id: string, updates: Partial<Folder>) => void;
addFolder: (folder: Omit<Folder, "id">, index?: number) => Folder;
deleteFolder: (id: string) => void;
}

export const createFolderSlice: StateCreator<FolderSlice> = (set, get) => ({
folders: [
{
id: 'HplOMyf2mDqvVMdphJbt',
name: 'My Sample Snippets',
description: 'This is a sample folder',
snippets: [
{
id: '5mJw031VPo2WxNIQyeXN',
name: 'Demo - Plain text',
content: 'be a software engineer',
shortcut: '/do',
},
{
id: '6mJw031VPo2WxNIQyeYN',
name: 'Demo - Styled Text',
content:
'be a translate expert, I will give you a sentence and help me translate to english',
shortcut: '/doT',
},
],
},
{
id: 'folder-1741057188488',
name: 'Test',
description: 'test',
snippets: [
{
id: 'snippet-1741057206823',
name: 'test',
content: '<p>New snippet content Test</p>',
shortcut: '/test',
},
],
},
],
setFolders: (folders) => set({ folders }),
updateFolder: (id, updates) =>
set({
folders: get().folders.map((folder) =>
folder.id === id ? { ...folder, ...updates } : folder
),
}),
// 新增資料夾,可選擇插入位置
addFolder: (folder, index) => {
const newFolder: Folder = { ...folder, id: `folder-${Date.now()}` };
set((state) => ({
folders:
typeof index === "number"
? [
...state.folders.slice(0, index),
newFolder,
...state.folders.slice(index),
]
: [...state.folders, newFolder],
}));
return newFolder;
},
deleteFolder: (id) =>
set({
folders: get().folders.filter((folder) => folder.id !== id),
}),
});
  • 使用 set 來更新狀態,get 取得當前狀態。
  • addFolder 方法可指定新增資料夾的位置(透過可選的 index 參數);若未指定,則預設新增到列表尾端。

snippetSlice.ts

snippetSlice 負責 snippet 的新增、刪除與更新操作。由於 snippets 存放於 folders 中,因此此 slice 會依賴 FolderSlice 的狀態來進行資料處理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// src/stores/snippet/slices/snippetSlice.ts
import { StateCreator } from 'zustand';
import { Snippet } from '@/types/snippets';
import { FolderSlice } from './folderSlice';

export interface SnippetSlice {
addSnippetToFolder: (folderId: string, snippet: Omit<Snippet, 'id'>) => Snippet;
deleteSnippetFromFolder: (folderId: string, snippetId: string) => void;
updateSnippet: (snippetId: string, updatedSnippet: Partial<Snippet>) => void;
}

// 這裡依賴 FolderSlice,因為 snippets 都儲存在 folders 內
// 修改後,addSnippetToFolder 回傳新 snippet
export const createSnippetSlice: StateCreator<
FolderSlice & SnippetSlice,
[],
[],
SnippetSlice
> = (set, get) => ({
// 新增一個 snippet 到指定的資料夾,並回傳新增的 snippet(包含自動生成的 id)
addSnippetToFolder: (folderId, snippet) => {
const newSnippet: Snippet = { ...snippet, id: `snippet-${Date.now()}` };
set({
folders: get().folders.map((folder) =>
folder.id === folderId
? { ...folder, snippets: [...folder.snippets, newSnippet] }
: folder
),
});
return newSnippet; // 回傳新增後完整的 snippet 物件
},
deleteSnippetFromFolder: (folderId, snippetId) =>
set({
folders: get().folders.map((folder) =>
folder.id === folderId
? {
...folder,
snippets: folder.snippets.filter(
(snippet) => snippet.id !== snippetId
),
}
: folder
),
}),
updateSnippet: (snippetId, updatedSnippet) =>
set({
folders: get().folders.map((folder) => ({
...folder,
snippets: folder.snippets.map((snippet) =>
snippet.id === snippetId ? { ...snippet, ...updatedSnippet } : snippet
),
})),
}),
});
  • 新增 snippet(addSnippetToFolder)

    提供一個資料夾 id 和 snippet 的內容(不含 id)。
    自動產生 snippet 的 id(透過 Date.now())。
    將新的 snippet 放到指定的資料夾內,並回傳完整的新 snippet,方便後續操作使用。

  • 刪除 snippet(deleteSnippetFromFolder)

    根據指定的資料夾 id 和 snippet id 移除 snippet。
    使用 filter 方法,確保其他 snippet 不受影響。

  • 更新 snippet(updateSnippet)

    -提供 snippet id 和欲更新的部分 snippet 屬性。
    -使用 map 方法,只更新符合指定 id 的 snippet,其他 snippet 維持不變。

uiSlice.ts

uiSlice 專門用來管理專案內與 UI 顯示相關的狀態,例如 dialog(對話框)的開啟或關閉狀態,以及當前匹配(matched)的 snippet 資訊。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/stores/snippet/slices/uiSlice.ts
import { StateCreator } from 'zustand';
import { MatchedSnippet } from '@/types/snippets';

export interface UISlice {
isDialogOpen: boolean;
matchedSnippet: MatchedSnippet;
setIsDialogOpen: (open: boolean) => void;
setMatchedSnippet: (snippet: MatchedSnippet) => void;
}

export const createUISlice: StateCreator<UISlice> = (set) => ({
isDialogOpen: false,
matchedSnippet: {
content: '',
targetElement: null,
insert: false,
shortcut: '',
},
setIsDialogOpen: (open: boolean) => set({ isDialogOpen: open }),
setMatchedSnippet: (snippet: MatchedSnippet) => set({ matchedSnippet: snippet }),
});
  • isDialogOpen
    負責追蹤 dialog 是否正在顯示,便於控制 dialog 元件的顯示與隱藏。
  • matchedSnippet
    用於暫時儲存用戶目前匹配到的 snippet 資訊,例如 snippet 的內容、目標元素、快捷鍵資訊等等,讓 UI 能即時反應用戶操作。

在頁面中使用 Zustand 管理的狀態

想在頁面或元件內使用剛才建立的 Zustand 狀態時,只需從我們整合好的 useSnippetStore 引入所需的狀態或方法即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { useSnippetStore } from '@/stores/snippet';
import { useEffect } from 'react';
import { Snippet } from '@/types/snippets';

function MyComponent() {
// 從整合後的 store 直接取出需要的方法
const { addFolder } = useSnippetStore();

useEffect(() => {
// 舉例:在元件載入時新增一個 folder
const newFolder = addFolder({
name: 'New Folder',
description: 'Example folder created on load',
snippets: [],
});

console.log('新增了資料夾:', newFolder);
}, [addFolder]);

return <div>你的元件內容</div>;
}

  • 不再需要額外引入任何 Context Provider。
  • 在任何需要存取全局狀態的地方,只需簡單導入 useSnippetStore,然後直接調用對應的方法或取得狀態即可。

結論

藉由導入 Zustand 並將狀態拆分成多個獨立的 Slice(folderSlice、snippetSlice、uiSlice),使我能夠:

  • 清晰地分離狀態責任:每個 Slice 專注於處理特定領域的邏輯,如資料夾管理、snippet 操作,以及 UI 狀態。
  • 靈活管理持久化策略:透過 Zustand 提供的 middleware(如 persist 與 partialize),只將必要的資料(例如資料夾)持久化至 localStorage,而 UI 等臨時狀態則保留在記憶體中。
  • 有效降低耦合度與樣板代碼:每個元件不再需要額外引入 Provider,簡化了開發流程,並且降低未來維護成本。