詳解Jotai Immer如何實現(xiàn)undo redo功能示例詳解
背景
之前項目中一直使用redux作為全局狀態(tài)庫使用,然后最近有個大功能改造,涉及undo、redo等功能影響,小伙伴推薦jotai來替代redux。于是稍事研究了下,對比redux,果然簡單了很多,去除了redux里厚重的模板代碼,使用也很方便。 所以為啥一開始不直接就jotai呢,還抱著redux啃了半天?反思了下,可能原因是redux名聲更響,當(dāng)年擼flutter、擼ios的時候就被這個名詞轟炸了,所以一想到全局狀態(tài)庫,就直接選了它,忽略了前端發(fā)展的日新月異,早就有了更好的替代品,經(jīng)驗主義作祟,引以為戒。另一個原因估計是論壇里光顧著摸魚沸點了,忽略了介紹jotai的文章。 小伙伴又推薦了結(jié)合immer的功能來實現(xiàn)undo、redo功能。好嘛,翻了翻文檔,immer不就是個簡化immutable對象修改的工具嗎?咋就跟undo、redo搭上關(guān)系了。經(jīng)解釋,方知原來immer高級篇有produceWithPatches的功能支持對象的undo以及redo功能。 當(dāng)然undo、redo功能我找了找其實是有現(xiàn)有庫的,只是代碼翻了翻多年沒更新了,下載過來連示例都運行不起來,算了,配合以上的jotai、immer功能,貌似直接簡單擼一個也不難的樣子,那么就開干吧。
代碼不多,直接上了
import { Patch, applyPatches, produceWithPatches } from "immer"; import { atomWithImmer } from "jotai-immer"; import { atom, createStore } from "jotai/vanilla"; import { JSONSchema7 } from "json-schema"; interface HistoryInfo { patch: Patch; inverse: Patch; } interface HistoryState { undo: HistoryInfo[]; redo: HistoryInfo[]; } // 業(yè)務(wù)代碼涉及的undo、redo數(shù)據(jù) export interface HistoryItem { businessA: { [key: string]: any }; businessB: Record<string, JSONSchema7>; businessC: any; } // 構(gòu)建一個undo、redo的數(shù)據(jù)起點 const historyItemAtom = atomWithImmer<HistoryItem>({ businessA: {}, businessB: {}, businessC: {}, }); // 觸發(fā)需要保存的undo的一個操作事件 export const fireHistoryAtom = atom(0.0); export const businessAAtom = atomWithImmer<{ [key: string]: any }>({}); export const businessBAtom = atomWithImmer<Record<string, JSONSchema7>>({}); export const businessCAtom = atomWithImmer<any>(); export const historyAtom = atomWithImmer<HistoryState>({ undo: [], redo: [], }); export const store = createStore(); // 頁面數(shù)據(jù)加載完畢寫入初始化history export const dataInit = () => { const newHis: HistoryItem = { businessA: store.get(businessAAtom), businessB: store.get(businessBAtom), businessC: store.get(businessCAtom), }; store.set(historyItemAtom, newHis); }; // ---------------------------------------------------------------- // atom subscriptions store.sub(fireHistoryAtom, () => { const newHis: HistoryItem = { businessA: store.get(businessAAtom), businessB: store.get(businessBAtom), businessC: store.get(businessCAtom), }; const oldHis = store.get(historyItemAtom); const [next, patch, inverse] = produceWithPatches(oldHis, (draft) => { draft = newHis; return draft; }); store.set(historyItemAtom, next); store.set(historyAtom, (draft) => { draft.undo.push({ patch: patch[0], inverse: inverse[0], }); draft.redo = []; }); }); export const fireHistory = () => { setTimeout(() => { store.set(fireHistoryAtom, Math.random()); }, 20); }; // 執(zhí)行業(yè)務(wù)代碼 const doAction = (item: HistoryItem) => { store.set(businessAAtom, (draft) => { draft = item.businessA; return draft; }); store.set(businessBAtom, (draft) => { draft = item.businessB; return draft; }); store.set(businessCAtom, (draft) => { draft = item.businessC; return draft; }); store.set(historyItemAtom, (draft) => { draft = item; return draft; }); }; export const undoAction = () => { const history = store.get(historyAtom); if (history.undo.length === 0) { return; } const old = history.undo[history.undo.length - 1]; const currentItem = store.get(historyItemAtom); const item = applyPatches(currentItem, [old.inverse]); doAction(item); store.set(historyAtom, (draft) => { const current = draft.undo.pop(); if (current) { draft.redo.push(current); } }); }; export const redoAction = () => { const history = store.get(historyAtom); if (history.redo.length === 0) { return; } const old = history.redo[history.redo.length - 1]; const currentItem = store.get(historyItemAtom); const item = applyPatches(currentItem, [old.patch]); doAction(item); store.set(historyAtom, (draft) => { const current = draft.redo.pop(); if (current) { draft.undo.push(current); } }); };
大致講下思路
定義 HistoryItem 作為undo、redo所需要恢復(fù)的業(yè)務(wù)數(shù)據(jù),可隨意擴展。undo、redo本質(zhì)上就是你點了undo按鈕后你的數(shù)據(jù)需要恢復(fù)到上一個狀態(tài)。當(dāng)業(yè)務(wù)復(fù)雜了之后,你一次操作可能包含了多個數(shù)據(jù)的變化,而你undo的操作應(yīng)該是把這些數(shù)據(jù)一起還原。所以把涉及到變化的數(shù)據(jù)都包裝在一起,形成一個historyitem。通過 immer提供的produceWithPatches生成撤銷和恢復(fù)數(shù)據(jù), 存在 HistoryState 的undo 里,然后你點擊undo按鈕,把數(shù)據(jù)從undo中取出,放入redo中。然后把數(shù)據(jù)狀態(tài)通過jotai全局修改,差不多就完成了。
簡單的jotai使用是不需要store參與的,這里為了取數(shù)據(jù)、改數(shù)據(jù)方便,所以使用了store的監(jiān)聽和set功能。 使用store的代碼也很簡單,直接在最外層包個provider即可
<Provider store={mainStore}> <MainPage /> </Provider>
使用方式應(yīng)該挺簡單的吧。 當(dāng)你做完需要undo的操作后,調(diào)用fireHistory()函數(shù)即可。頁面的undo、redo按鈕可以直接把事件映射到undoAction、redoAction。至于undo、redo按鈕是否能點的功能可以直接讀取 historyAtom 里面undo、redo的數(shù)組長度。
以上就是詳解Jotai Immer如何實現(xiàn)undo redo功能示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Jotai Immer實現(xiàn)undo redo的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
解決React報錯useNavigate()?may?be?used?only?in?context?of
這篇文章主要為大家介紹了解決React報錯useNavigate()?may?be?used?only?in?context?of?Router,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12react使用useImperativeHandle示例詳解
這篇文章主要為大家介紹了react使用useImperativeHandle示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09React中useEffect與生命周期鉤子函數(shù)的對應(yīng)關(guān)系說明
這篇文章主要介紹了React中useEffect與生命周期鉤子函數(shù)的對應(yīng)關(guān)系說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09React Refs轉(zhuǎn)發(fā)實現(xiàn)流程詳解
Refs是一個 獲取 DOM節(jié)點或React元素實例的工具,在React中Refs 提供了一種方式,允許用戶訪問DOM 節(jié)點或者在render方法中創(chuàng)建的React元素,這篇文章主要給大家介紹了關(guān)于React中refs的一些常見用法,需要的朋友可以參考下2022-12-12通過React-Native實現(xiàn)自定義橫向滑動進(jìn)度條的 ScrollView組件
開發(fā)一個首頁擺放菜單入口的ScrollView可滑動組件,允許自定義橫向滑動進(jìn)度條,且內(nèi)部渲染的菜單內(nèi)容支持自定義展示的行數(shù)和列數(shù),在內(nèi)容超出屏幕后,渲染順序為縱向由上至下依次排列,對React Native橫向滑動進(jìn)度條相關(guān)知識感興趣的朋友一起看看吧2024-02-02React替換傳統(tǒng)拷貝方法的Immutable使用
Immutable.js出自Facebook,是最流行的不可變數(shù)據(jù)結(jié)構(gòu)的實現(xiàn)之一。它實現(xiàn)了完全的持久化數(shù)據(jù)結(jié)構(gòu),使用結(jié)構(gòu)共享。所有的更新操作都會返回新的值,但是在內(nèi)部結(jié)構(gòu)是共享的,來減少內(nèi)存占用2023-02-02React組件實例三大屬性state props refs使用詳解
這篇文章主要為大家介紹了React組件實例三大屬性state props refs使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09