· Joseph · frontend  · 5 min read

讓sessionStorge結合useSyncExternalStore,一更新其他components都知道

前一陣子遇到一個問題,一直在嘗試如何讓sessionStorage / localStorage / cookie有變動時,react遙遠的其他元件也可以知道其變化。
於是乎就找到了一個codeSandbox,其原理就是透過 useSyncExternalStore 去通知有subscribe的元件,讓他們知道sessionStorage / localStorage / cookie key-value有變動,

前一陣子遇到一個問題,一直在嘗試如何讓sessionStorage / localStorage / cookie有變動時,react遙遠的其他元件也可以知道其變化。 於是乎就找到了一個codeSandbox,其原理就是透過 useSyncExternalStore 去通知有subscribe的元件,讓他們知道sessionStorage / localStorage / cookie key-value有變動,

sessionStore

原先codeSandbox的寫法裡,會發現subscribe會add到不屬於同個itemName的listener,導致notify forEach的次數增加。雖然最終會因為useSyncExternalStore裡checkIfSnapshotChanged沒變,讓實際update到的元件維持在被改變sessionStorage key-value pairs的元件。不過讓forEach少跑一點也不是件壞事。

這邊稍微改一下讓cache可以跟sessionStorage一樣的key-value pairs,同時讓subscribe需要通知的listeners只有key對應到的useSyncExternalStore。做法就是先讓sessionStore改為higher-order function,讓第一階(first-order)有個cache,其儲存的內容與sessionStorage get, set, and remove 一致,然後回傳higher-order sessionStore,讓他們各自有各自的listeners。最後要export出來的其實是 const store = sessionStore();,這樣就完成第一部的改寫了。

可以觀察notify裡listeners.size的數量

reactiveSessionStorage/sessionStore.ts

type Listener = () => void;

// higher-order function
const sessionStore = () => {
  // First-order scoped, create cache when sessionStore initialized
  const cache = new Map<string, any>();

  return () => {
    // Higher-order scoped, each store has a listeners
    const listeners = new Set<Listener>();
    const subscribe = (listener: Listener) => {
      listeners.add(listener);
      return () => {
        listeners.delete(listener);
      };
    };

    const notify = () => listeners.forEach((l) => l());

    const get = <T>(itemName: string) => {
      const value = cache.get(itemName);
      if (value) return value;
      const sessionValue = sessionStorage.getItem(itemName);
      if (!sessionValue) return undefined;

      const cachedValue = JSON.parse(sessionValue) as T;
      cache.set(itemName, cachedValue);
      return cachedValue;
    };

    const update = <T>(itemName: string, v: T) => {
      cache.set(itemName, v);
      sessionStorage.setItem(itemName, JSON.stringify(v));
      notify();
    };

    const remove = (itemName: string) => {
      cache.delete(itemName);
      sessionStorage.removeItem(itemName);
      notify();
    };

    return {
      subscribe,
      get,
      update,
      remove,
    };
  };
};

// export this store
const store = sessionStore();

export type SessionStoreType = ReturnType<typeof store>;
export default store;

Singleton class

再做個higher-order function去create storage讓storeMap在first-order function內,然後return (itemName: string) => SessionStoreType形式的function。

這邊也可以改寫成Singleton class + getStore function,讓我們不必重複建立sessionStore,後面也可以直接呼叫singleton getStore來操作先前建立過的sessionStore的get, set, update function

reactiveSessionStorage/createReactiveSessionStorage.ts

import sessionStore from './sessionStore';
import type { SessionStoreType } from './sessionStore';

const createReactiveSessionStorage = () => {
  const storeMap: Map<string, SessionStoreType> = new Map();
  return (itemName: string) => {
    if (storeMap.get(itemName)) return storeMap.get(itemName);
    storeMap.set(itemName, getSessionStore());
    return storeMap.get(itemName);
  };
};

export const getStorage = createReactiveSessionStorage();

custom hooks: useReactiveSessionStorage, useSessionActions

最後,把他們跟useSyncExternalStore綁在一起,變成一個custom hook,

  • useReactiveSessionStorage訂閱useSyncExternalStore,同時return value, update function, remove function
  • useSessionActions,很純粹的操作 getStorage(itemName),不依賴useSyncExternalStore

reactiveSessionStorage/useReactiveSessionStorage.ts

'use client';
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';

import type { SessionStoreType } from './sessionStore';
import { getStorage } from './createReactiveSessionStorage';

const useReactiveSessionStorage = <T>(itemName: string) => {
  const storeRef = useRef<SessionStoreType>(getStorage(itemName));

  const [selector, update, remove] = useMemo(
    () => [
      // selector
      (): T => storeRef.current?.get<T>(itemName),
      // update
      (v: T) => storeRef.current?.update<T>(itemName, v),
      // remove
      () => storeRef.current?.remove(itemName),
    ],
    [itemName]
  );

  const value = useSyncExternalStore(storeRef.current.subscribe, selector, selector);
  return [value, update, remove] as const;
};

export const useSessionActions = <T>(itemName: string) => {
  const actions = useMemo(
    () => [
      // update
      (v: T) => getStorage(itemName).update<T>(itemName, v),
      // remove
      () => getStorage(itemName).remove(itemName),
    ],
    [itemName]
  );

  return actions;
};

export default useReactiveSessionStorage;

這樣一個設計就可以讓sessionStorage有變動的時候也update使用useReactiveSessionStorage custom hook的元件。

那再來看看chatGPT怎麼說: https://chatgpt.com/s/t_6854dd517540819187228164a5ebe8ef

然後看看perplexity怎麼說: https://www.perplexity.ai/search/how-to-use-session-with-usesyn-hUmoT5lMQIeDIAz31ysNpQ#0

AI的方法比較傾向使用window event listerner,Perplexity又相對簡潔,更換storage只要換eventListener就好。而我的做法是換storage higher-order function。除了這些方法外,zustand state management套件也有Persisting store可以用,三種寫法各有利弊,再讓需要的人看看。

References

  1. reactive-cookie-store: https://codesandbox.io/p/sandbox/reactive-cookie-store-0g67ps
  2. Zustand: https://zustand.docs.pmnd.rs/integrations/persisting-store-data#simple-example
Back to Blog

Related Posts

View All Posts »

A powerful react hook - useSyncExternalStore

It is an interesting hook called useSyncExternalStore came from React 18. After I went through the doc, I had totally no idea about how to use it and why it works. Luckily, I got a task I thought I can use this hook, and meanwhile I traced the source code to understand useSyncExternalStore implementation. In this article, I will explain how it works internally and show a demo which is differ from the doc. TOC

I18n in NextJS 13 app dir

I used to implement I18n with next-i18next in Next.js. However, after upgrading Next.js 13, there is no need for next-i18next. Instead, you can directly use i18next and react-i18next by this tutorial. Meanwhile, the official doc shows how to use internationalization in app dir. and also provide an example of using next-intl. In this blog, I'll demostrate how to use next-intl in Next.js 13 app dir.

How to use React hook - useImperativeHandle

How to use React hook - useImperativeHandle

Today, We're going to introduce the way to use a great and useful React hook - useImperativeHandle. Have you ever think what is meaning of Imperative? Imperative In grammar, a clause that is in the imperative, or in the imperative mood, contains the base form of a verb and usually has no subject. Examples are ' Go away' and ' Please be careful'. Clauses of this kind are typically used to tell someone to do something. So we can just think useImperativeHandle is Let ref access the handle. Go back to useImperativeHandle definition: useImperativeHandle is a React Hook that lets you customize the handle exposed as a ref. Right? Okay, before starting it, we have to recap how we do if we don't use it.

Switching from vim to Neovim!

Switching from vim to Neovim!

I've been using Vim for a long time and finally switched to Neovim. The initial thought of switching came after the author of VIM passed away in August 2023, as I didn’t have the time to try other editors. After a year, “vibe coding” grew up, so I started thinking how to integrate AI into my editor and surveying how to use AI in Vim, which led to this journey. TOC Main differences Tab vs. Buffer: ref: https://www.reddit.com/r/neovim/comments/13iy0p0/why_some_people_use_buffers_for_tabs_and_not_vim/ In vim, I used vsp or tabnew to open or edit a file and mapped tabprevious command with Tab key to navigate between tabs. However, in Neovim, I used buffer instead of tabs and mapped BufferLineCyclePrev with Shift + h for switching buffers. coc vs. native lsp: I configured many coc settings to support TypeScript and JavaScript language servers, including linting and Prettier on save, go-to definition or reference, and codelens. After using Neovim, I converted all of these settings to nvim-lspconfig and mason, among others. Lua supports: Although I’m not familiar with Lua, it allows me to write more readable configuration files using modules, functions, and tables (objects) for Neovim. AI supports: Vibe coding! I found many plugins to integrate LLMs and finally selected three to use: avante.nvim Let your Neovim be a Cursor AI IDE – it's undergoing rapid iterations and many exciting features will be added successively; it updates almost every day! CodeCompanion CodeCompanion can customize workflows and prompts, and its Action Palette is a really useful tool. gp.nvim GP means GPT Prompt, and it's an Neovim AI plugin. It helps me write prompts through Neovim's cmdline. I usually switch between these plugins, and I'm still thinking about my 'vibe-way' to use them. Because of supporting Ollama LLM, all of them can be used offline. For now, I've used Neovim for three months, and got to know Neovim. In my experience, Neovim is really more productive than vim. Reference AI in Neovim How to set up Neovim for coding React ZazenCodes Neovim Rewrite by Avante + Ollama gemma3:4b ------ For a long time, I relied on Vim for my coding. However, after Bram Moolenaar’s passing in August 2023 – a significant influence in the Vim community – I decided it was time for a change. I began exploring alternative editors, ultimately settling on Neovim. This journey wasn’t just about switching editors; it was about integrating AI into my workflow and redefining my coding experience. Key Differences: Tabs vs. Buffers One of the first things I noticed was the shift from tabs to buffers in Neovim. In Vim, I frequently used vsp or tabnew to open or edit files, navigating between tabs with the Tab key and Tabprevious command. Neovim, however, utilizes buffers, offering a more streamlined approach. I configured BufferLineCyclePrev with Shift + h for seamless buffer switching, alongside nvim-tree/nvim-web-devicons and akinsho/bufferline.nvim. Leveraging Language Servers with coc and nvim-lspconfig I configured many coc settings to support TypeScript and JavaScript language servers, including linting and Prettier on save, go-to definition or reference, and codelens. Recognizing the power of language servers, I then converted all of these settings to nvim-lspconfig and mason.nvim, streamlining my development environment. Lua Configuration for Readability Although I’m relatively new to Lua, it allows me to write more readable configuration files using modules, functions, and tables (objects) for Neovim. Here’s a snippet of my configuration: AI-Powered Productivity with avante.nvim, CodeCompanion, and gp.nvim To truly elevate my coding experience, I integrated several AI plugins. I selected: avante.nvim**: This plugin transforms Neovim into a Cursor AI IDE, undergoing rapid iterations and adding exciting new features daily. CodeCompanion**: This plugin allows for customizable workflows and prompts, with a particularly useful Action Palette. gp.nvim**: (GPT Prompt) – This plugin helps me write prompts through Neovim’s command line interface, leveraging folke/noice.nvim. Because of supporting Ollama LLM, all of these plugins can be used offline. I’m still experimenting with how to best utilize these plugins – a “vibe-way” to coding! Resources for Further Exploration AI in Neovim How to set up Neovim for coding React ZazenCodes Neovim