重大事項
📣 :重大事項提前通知!快來圍觀,不容錯過!
極限科技 一直致力於為開發者和企業提供優質的開源工具,提升整個技術生態的活力。除了維護國內最流行的分詞器 analysis-ik 和 analysis-pinyin,也在不斷推動更多高質量開源產品的誕生。
在極限科技成立三週年之際,公司宣佈以下產品和工具已全面開源:
- INFINI Framework
- INFINI Gateway
- INFINI Console
- INFINI Agent
- INFINI Loadgen
- INFINI Coco AI
以上開源軟件都可以在 Github 上面找到: https://github.com/infinilabs
希望大家都能給個免費的 Star🌟 支持一下!!!
背景
在開發公司項目 INFINI Cloud(暫未開源,敬請期待。 不過此次開源的同類項目有 INFINI Console)的時候,該項目上有個更改時區的全局組件,同時還有一個可以更改時區的局部組件,想讓更改時區的時候能聯動起來,實時響應起來。
Tip:如果有人對該時間組件感興趣,可以移步 https://github.com/infinilabs/ui-common,同時也希望收到您 Star🌟 支持,也希望和大家一起共建。
其實每次設置完時區的數據之後是存在了前端的 localStorage 裏邊,時間組件裏邊也是從 localStorage 拿去默認值來回顯。如果當前頁面不刷新,那麼時間組件就不能更新到最新的 localStorage 數據。
怎麼才能讓 localStorage 存儲的數也變成響應式呢?
實現
- 應該寫個公共的方法,不僅僅時區數據能用,萬一後邊其他數據也能用。
- 項目是 React 項目,那就寫個 hook
- 怎麼才能讓 localStorage 數據變成響應式呢?監聽?
失敗的案例 1
首先想到的是按照下邊這種方式做,
useEffect(() => {
console.log(11111, localStorage.getItem("timezone"));
}, [localStorage.getItem("timezone")]);
得到的測試結果肯定是失敗的,但是為啥失敗?我們也應該知道一下。查了資料説,使用 localStorage.getItem('timezone') 作為依賴項會導致每次渲染都重新計算依賴項,這不是正確的做法。
具體看一下官方文檔:useEffect(setup, dependencies?)
在此説一下第二個參數 dependencies:
可選 dependencies:setup 代碼中引用的所有響應式值的列表。響應式值包括 props、state 以及所有直接在組件內部聲明的變量和函數。如果你的代碼檢查工具 配置了 React,那麼它將驗證是否每個響應式值都被正確地指定為一個依賴項。依賴項列表的元素數量必須是固定的,並且必須像 [dep1, dep2, dep3] 這樣內聯編寫。React 將使用 Object.is 來比較每個依賴項和它先前的值。如果省略此參數,則在每次重新渲染組件之後,將重新運行 Effect 函數。
- 如果你的一些依賴項是組件內部定義的對象或函數,則存在這樣的風險,即它們將 導致 Effect 過多地重新運行。要解決這個問題,請刪除不必要的 對象 和 函數 依賴項。你還可以 抽離狀態更新 和 非響應式的邏輯 到 Effect 之外。
如果你的 Effect 依賴於在渲染期間創建的對象或函數,則它可能會頻繁運行。例如,此 Effect 在每次渲染後重新連接,因為 createOptions 函數 在每次渲染時都不同:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState("");
function createOptions() {
// 🚩 此函數在每次重新渲染都從頭開始創建
return {
serverUrl: serverUrl,
roomId: roomId,
};
}
useEffect(() => {
const options = createOptions(); // 它在 Effect 中被使用
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🚩 因此,此依賴項在每次重新渲染都是不同的
// ...
}
失敗的案例 2
一開始能想到的是監聽,那就用 window 上監聽事件。
在 React 應用中監聽 localStorage 的變化,可以使用 window 對象的 storage 事件。這個事件在同一域名的不同文檔之間共享,當某個文檔修改 localStorage 時,其他文檔會收到通知。
寫代碼...
// useRefreshLocalStorage.js
import { useState, useEffect } from "react";
const useRefreshLocalStorage = (key) => {
const [storageValue, setStorageValue] = useState(localStorage.getItem(key));
useEffect(() => {
const handleStorageChange = (event) => {
if (event.key === key) {
setStorageValue(event.newValue);
}
};
window.addEventListener("storage", handleStorageChange);
return () => {
window.removeEventListener("storage", handleStorageChange);
};
}, [key]);
return [storageValue];
};
export default useRefreshLocalStorage;
使用方式:
// useTimezone.js
import { useState, useEffect } from "react";
import { getTimezone, timezoneKey } from "@/utils/utils";
import useRefreshLocalStorage from "./useRefreshLocalStorage";
function useTimezone() {
const [TimeZone, setTimeZone] = useState(() => getTimezone());
const [storageValue] = useRefreshLocalStorage(timezoneKey);
useEffect(() => {
setTimeZone(() => getTimezone());
}, [storageValue]);
return [TimeZone];
}
export default useTimezone;
經過測試,失敗了,沒有效果!!!那到底怎麼回事呢?哪裏出現問題了?查閲資料經過思考,可能出現的問題的原因有:只能監聽同源的兩個頁面之間的 storage 變更,沒法監聽同一個頁面的變更。
成功的案例
import { useState, useEffect } from "react";
// 自定義 Hook,用於監聽 localStorage 中指定鍵的變化
function useRefreshLocalStorage(localStorage_key) {
// 檢查 localStorage_key 是否有效
if (!localStorage_key || typeof localStorage_key !== "string") {
return [null];
}
// 創建一個狀態變量來保存 localStorage 中的值
const [storageValue, setStorageValue] = useState(
localStorage.getItem(localStorage_key)
);
useEffect(() => {
// 保存原始的 localStorage.setItem 方法
const originalSetItem = localStorage.setItem;
// 重寫 localStorage.setItem 方法,添加事件觸發邏輯
localStorage.setItem = function (key, newValue) {
// 創建一個自定義事件,用於通知 localStorage 的變化
const setItemEvent = new CustomEvent("setItemEvent", {
detail: { key, newValue },
});
// 觸發自定義事件
window.dispatchEvent(setItemEvent);
// 調用原始的 localStorage.setItem 方法
originalSetItem.apply(this, [key, newValue]);
};
// 事件處理函數,用於處理自定義事件
const handleSetItemEvent = (event) => {
const customEvent = event;
// 檢查事件的鍵是否是我們關心的 localStorage_key
if (event.detail.key === localStorage_key) {
// 更新狀態變量 storageValue
const updatedValue = customEvent.detail.newValue;
setStorageValue(updatedValue);
}
};
// 添加自定義事件的監聽器
window.addEventListener("setItemEvent", handleSetItemEvent);
// 清除事件監聽器和還原原始方法
return () => {
// 移除自定義事件監聽器
window.removeEventListener("setItemEvent", handleSetItemEvent);
// 還原原始的 localStorage.setItem 方法
localStorage.setItem = originalSetItem;
};
// 依賴數組,只在 localStorage_key 變化時重新運行 useEffect
}, [localStorage_key]);
// 返回當前的 storageValue
// 為啥沒有返回 setStorageValue ?
// 因為想讓用户直接操作自己真實的 “setValue” 方法,這裏只做一個只讀。
return [storageValue];
}
export default useRefreshLocalStorage;
具體的實現步驟如上,每一步也加上了註釋。
接下來就是測試了,
useTimezone 針對 timezone 數據統一封裝,
// useTimezone.js
import { useState, useEffect } from "react";
import { getTimezone, timezoneKey } from "@/utils/utils";
import useRefreshLocalStorage from "./useRefreshLocalStorage";
function useTimezone() {
const [TimeZone, setTimeZone] = useState(() => getTimezone());
const [storageValue] = useRefreshLocalStorage(timezoneKey);
useEffect(() => {
setTimeZone(() => getTimezone());
}, [storageValue]);
return [TimeZone];
}
export default useTimezone;
具體的業務頁面組件中使用,
// 頁面中
// ...
import useTimezone from "@/hooks/useTimezone";
export default (props) => {
// ...
const [TimeZone] = useTimezone();
useEffect(()=>{
console.log(11111, TimeZone)
},[TimeZone)
}
測試結果必須是成功的啊!!!
小結
其實想要做到該效果,用全局 store 狀態管理也能做到,條條大路通羅馬嘛!不過本次需求由於歷史原因一直使用的是 localStorage ,索性就想着 如何讓 localStorage 存儲變為響應式 ?
不知道大家還有什麼更好的方法嗎?
作者:Rain9,極限科技(INFINI Labs) 高級前端開發工程師。