關於數據持久化,以Redux、zustand、vuex、pinia為例

> subscribe function 你有聽過嗎?

關於數據持久化,以Redux、zustand、vuex、pinia為例

數據持久化

關於數據持久基本是前端許多功能實現上必須了解的東西,常見的如 localStorage、sessionStorage、indexedDB,儲存例如一些用戶資料、記憶狀態、版本資訊、不太會變動的數據....

例如我們重整頁面的時候,我們會不希望

  • redux 中的儲存的數據被清空
  • client 端已經拿過這些資料的話就不要發請求,不要每次都從 sever 拿資料減少流量

TanStack Query、SWR,這兩個 library 也可以達到類似的效果,視你的專案而定。

要實現持久化, 最簡單的方法是 google search type 套件名稱 persist 就能找到相對應的 library 可以使用,而這篇將以 Redux 為例,講解熱門 state mangerment library 如何不使用其他多餘的套件自己控制持久化數據,

各套件相對應的 library: redux-persist、vuex-persist、pinia-plugin-persistedstate

Core

現今熱門的狀態管理套件基本上都能找到subscribe函數,來監聽 state 的變化,甚至如 zustand 都出了 middleware 可以直接使用 persist,相當於內建在裡面。

以下例子為 React、Vue,常用的狀態管理套件,redux、zustand、vuex、pinia

此篇的核心就是利用 subscribe 的監聽將變動的 state,儲存至 localStorage,然後在初始化 APP 的 create store 的時候再將 localStorage 中的資料拿出來當作初始資料。


這邊我們先寫一個 util 的 function,用來跟 localStorage(依需求選擇使用 localStorage 或是 sessionStorage) 交互

// utils.js

/** 讀取 store
 *
 * @returns {any|undefined}
 */
export const loadState = () => {
  try {
    // localStorage 的 key name 可以自己定義或是由傳入的 arguments 來決定,
    // 這邊我以 "state" 固定當作 key name
    const s = localStorage.getItem('state')
    if (s == null) {
      return undefined
    }
    return JSON.parse(s)
  } catch (err) {
    console.warn('load state failed', err)
    return undefined
  }
}

/**
 * 存儲 store
 * @param {object} state
 */
export const saveState = (state) => {
  try {
    const s = JSON.stringify(state)
    localStorage.setItem('state', s)
  } catch (err) {
    console.warn('save state failed', err)
  }
}

Redux

// store.js

import { loadState, saveState } from '@/utils'
import { configureStore } from '@reduxjs/toolkit'
import { combineReducers } from 'redux'

// Slice
import siteReducer from './slice/site'
import todoReducer from './slice/todo'

const reducer = combineReducers({
  todo: todoReducer,
  site: siteReducer,
})

// 從 localStorage 拿取上次儲存的資料
const preloadedState = loadState()

const store = configureStore({
  reducer,
  devTools: process.env.NODE_ENV !== 'production',
  preloadedState,
})

store.subscribe(() => {
  // 第一種寫法: 儲存所有的 state
  const state = store.getState()
  saveState(state)

  // 第二種寫法: 只想儲存某些 reducer (or slice)
  // const { site } = store.getState()
  // saveState({ site })
})

export default store

zustand

因為本身就有提供 persist 的 middleware 了,所以這邊我們直接拿官方的例子來用

// https://docs.pmnd.rs/zustand/integrations/persisting-store-data
import { create } from 'zustand'
import { createJSONStorage, persist } from 'zustand/middleware'

export const useBearStore = create(
  persist(
    (set, get) => ({
      bears: 0,
      addABear: () => set({ bears: get().bears + 1 }),
    }),
    {
      name: 'food-storage', // name of the item in the storage (must be unique)
      storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
    }
  )
)

vuex

// store.js
// vue^2.6.14 + vuex 3.6.2

import { loadState, saveState } from '@/utils'
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    count: 0,
  },
  mutations: {
    increment(state) {
      state.count++
    },
  },
})

if (localStorage.getItem('state')) {
  store.replaceState({ ...store.state, ...loadState() })
}

store.subscribe((_mutation, state) => {
  saveState(state)
})

export default store

pinia

  • 利用 $patch 將已儲存的資料塞回去 state
  • 利用 $subscribe 來監聽 state 的最新狀態並保存在 localStorage 裡
// store/count.ts
// vue^3.3.2 + pinia^2.0.36

import { loadState, saveState } from '@/utils'
import { defineStore, getActivePinia } from 'pinia'
import { computed, ref } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    count.value++
  }

  return { count, doubleCount, increment }
})

const subscribeToStore = async () => {
  // 當 pinia 初始化後才開始工作
  while (!getActivePinia()) {
    await new Promise((resolve) => requestAnimationFrame(resolve))
  }

  useCounterStore().$patch(loadState())

  useCounterStore().$subscribe((mutation, state) => {
    console.log({ mutation, state })
    saveState(state)
  })
}

subscribeToStore()

結論

subscribe 通常是大家在使用狀態管理套件中比較少去注意到的函數,然後不只是只能作持久化,拿來作監測 state 的變化也非常好用。