在前端開發中,動畫與互動是提升用戶體驗的關鍵元素。然而,當動畫與元件狀態、生命周期交織在一起時,實作上就可能遇到各種挑戰。
這篇文章記錄我在一個 React 專案中,解決「動畫重複觸發與重置」問題的過程,深入比較了使用 key 重置與 CSS Reflow(offsetWidth) 重置的方式
問題概述
在這個專案中,我有兩個自訂的元件,各自內部包含一個相同結構的 <input>
。當使用者點擊某個操作後,我希望這些 input:
練習來自 labuladong 的演算法筆記 的滑動視窗演算法核心程式碼模板。
用兩個指針來界定一個區間,也就是一個「窗口」:
窗口內的資料會隨著 left 和 right 的移動而動態改變。解題的關鍵在於:
1 | var slidingWindow = function(s) { |
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
1 | npm install zustand |
如果想使用持久化功能,也可以安裝:
1 | npm install zustand/middleware |
在專案的 src 資料夾下建立一個 store 資料夾,這之中包含給 folder , snippet 的 slice。之所以採用 slice 是因為原本 snippet 的處理邏輯較為複雜,包含了 folder 的儲存、內部 snippet 的儲存,以及一些專門給 dialog 使用的 UI 狀態。透過將 store 分拆成多個 slice(例如 folderSlice 與 uiSlice),每個模組都只負責處理自己相關的 state 與操作,能更好地維護代碼的結構與可讀性。
1 | src/ |
在專案中的 snippet 資料夾內,會建立一個 index.ts 檔案來整合所有獨立的 slice,並導出一個完整的 Zustand store。這麼做的優點是:之後在任何元件中需要使用狀態時,只需導入整個 store,而不需要逐一引入每個 slice
1 | // src/stores/snippet/index.ts |
在建立 folderSlice 時,透過 Zustand 的 StateCreator 定義了一個 slice,專門負責管理資料夾(folders)相關的狀態與邏輯。透過這個方式,能清楚地將資料夾的操作行為集中管理。
1 | // src/stores/snippet/slices/folderSlice.ts |
snippetSlice 負責 snippet 的新增、刪除與更新操作。由於 snippets 存放於 folders 中,因此此 slice 會依賴 FolderSlice 的狀態來進行資料處理。
1 | // src/stores/snippet/slices/snippetSlice.ts |
新增 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 專門用來管理專案內與 UI 顯示相關的狀態,例如 dialog(對話框)的開啟或關閉狀態,以及當前匹配(matched)的 snippet 資訊。
1 | // src/stores/snippet/slices/uiSlice.ts |
想在頁面或元件內使用剛才建立的 Zustand 狀態時,只需從我們整合好的 useSnippetStore 引入所需的狀態或方法即可
1 | import { useSnippetStore } from '@/stores/snippet'; |
藉由導入 Zustand 並將狀態拆分成多個獨立的 Slice(folderSlice、snippetSlice、uiSlice),使我能夠:
有新東西就是要來快速嘗試一下~~ 注意 cursor 要是付費版才能使用
至 GitHub clone 專案 Cursor Talk to Figma MCP
然後遵循 README:Get Started 開始
它需要安裝 bun 套件管理工具
1 | curl -fsSL https://bun.sh/install | bash |
需要在你的專案根目錄執行:
1 | cd /path/to/your/cursor-talk-to-figma-mcp |
1 | # 啟動 WebSocket server(必須持續執行) |
1 | // Add the server to your Cursor MCP configuration in ~/.cursor/mcp.json: |
以下為圖示:
在 Vue 中使用動態事件名稱的功能自 Vue 2.6.0 版本開始支持。可以使用 v-on 或 @ 來動態綁定事件名稱
簡單舉例:
根據裝置類型切換事件(桌機 vs 手機)
1 | <script setup> |
2.「檔案卡片」元件預覽或跳轉:
點擊 (click):選擇/下載檔案
滑鼠移入 (mouseover):預覽檔案摘要
1 | <template> |
1 | <script setup lang="ts"> |
注意:在「同一欄位會因不同情境而切換觸發事件」時,才建議用動態綁定。若只是單純切換事件名稱,建議直接在方法內判斷即可。
也可參考 Michael Hoffmann 這裡的另一實作方式:
https://stackblitz.com/edit/vue-tip-dynamic-event-names?file=src%2FApp.vue
在開發 React 或 Vue 應用時,動態組合 Tailwind CSS class 是常見的需求。但當 class 來源多樣(例如 props 傳入、條件渲染等),你可能會遇到 class 重複、衝突或難以維護的問題,這時候,twMerge 就能幫助你解決這些困難。
twMerge
是一個專為 Tailwind CSS 設計的 class 合併工
安裝方式:
1 |
|
基本使用範例:
1 |
|
一個 class 別應該只有一個職責,並且只應該因為一個理由而改變。
C# 程式碼範例:
以違反 SRP 的範例:
在這個範例中,User 類別同時負責使用者資料的管理、資料庫儲存、發送歡迎信和使用者驗證等多個職責。這違反了 SRP。
1 |
|
符合 SRP 的範例:
1 |
|
在這個範例中,我們將不同的職責分離到不同的類別中:
在前端開發中,我們經常使用元件 (Component) 的概念來構建使用者介面。一個元件通常負責渲染一部分的 UI,並處理與該部分 UI 相關的互動。這與 SRP 的概念非常相似。一個好的前端元件應該只負責單一的職責,例如:
顯示資料: 只負責接收資料並將其渲染到 UI 上。
處理使用者輸入: 只負責處理使用者在介面上的輸入事件,例如按鈕點擊、表單提交等。
發送 API 請求: 只負責向後端發送 API 請求並處理回應。
除了基礎運算子(如 Where、Select 和 OrderBy),LINQ 還提供了許多進階運算子,用於解決更複雜的查詢需求。以下是一些使用頻率較高的進階運算子,以及它們的功能與應用場景。
範例:投影字串清單中每個字串的第一個字母。
1 | List<string> words = ["an", "apple", "a", "day"]; |
SelectMany 將一個集合中的子集合展平(Flatten),生成單一層級的結果序列。它非常適合處理嵌套集合。
適用場景:需要從多個子集合中提取元素,並將它們組合為單一集合。
1 | List<string> phrases = ["an apple a day", "the quick brown fox"]; |
1 | List<string> phrases = ["an apple a day", "the quick brown fox"]; |
在提供一個,處理多對多關係數據(如學生和他們的選修課程)。
範例:攤平學生的課程清單
1 |
|
輸出結果
1 |
|
C# 的 LINQ (Language Integrated Query,語言整合查詢) 是一個非常強大的功能,它讓你可以使用類似 SQL 的語法來查詢各種不同的資料來源。這意味著你可以用統一的方式來處理資料,無論資料是來自陣列、列表、資料庫、XML 或其他任何支援 LINQ 的來源。
LINQ 有兩種主要的語法形式: