Safer way to interact with localStorage (#2679)

This commit is contained in:
Greg Bergé
2025-01-07 15:51:23 +01:00
committed by GitHub
parent d66c184ed2
commit 5c87ec7c2f
4 changed files with 67 additions and 28 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'gitbook': patch
---
Implement a safer way to interact with localStorage.
If it's disabled on the browser it should not throw error.
@@ -3,6 +3,7 @@
import React, { useCallback, useMemo } from 'react';
import { useHash, useIsMounted } from '@/components/hooks';
import * as storage from '@/lib/local-storage';
import { ClassValue, tcls } from '@/lib/tailwind';
interface TabsState {
@@ -12,15 +13,12 @@ interface TabsState {
activeTitles: string[];
}
let globalTabsState: TabsState = (() => {
if (typeof localStorage === 'undefined') {
return { activeIds: {}, activeTitles: [] };
}
const stored = localStorage.getItem('@gitbook/tabsState');
return stored ? (JSON.parse(stored) as TabsState) : { activeIds: {}, activeTitles: [] };
})();
const defaultTabsState: TabsState = {
activeIds: {},
activeTitles: [],
};
let globalTabsState = storage.getItem('@gitbook/tabsState', defaultTabsState);
const listeners = new Set<() => void>();
function useTabsState() {
@@ -33,9 +31,7 @@ function useTabsState() {
const setTabsState = useCallback((updater: (previous: TabsState) => TabsState) => {
globalTabsState = updater(globalTabsState);
if (typeof localStorage !== 'undefined') {
localStorage.setItem('@gitbook/tabsState', JSON.stringify(globalTabsState));
}
storage.setItem('@gitbook/tabsState', globalTabsState);
listeners.forEach((listener) => listener());
}, []);
const state = React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
@@ -1,5 +1,7 @@
'use client';
import * as storage from '@/lib/local-storage';
import { generateRandomId } from './utils';
const SESSION_TTL = 1000 * 60 * 30; // 30 minutes
@@ -22,22 +24,20 @@ export function getSession(): Session {
}
try {
const rawSession =
typeof localStorage !== 'undefined' ? localStorage.getItem(SESSION_KEY) : null;
const session = storage.getItem<unknown | null>(SESSION_KEY, null);
if (rawSession) {
const storedSession = JSON.parse(rawSession);
if (
typeof storedSession === 'object' &&
typeof storedSession.lastActiveAt === 'number' &&
typeof storedSession.id === 'string' &&
storedSession.lastActiveAt + SESSION_TTL > Date.now()
) {
currentSession = storedSession as Session;
touchSession();
return currentSession;
}
if (
session &&
typeof session === 'object' &&
'lastActiveAt' in session &&
typeof session.lastActiveAt === 'number' &&
'id' in session &&
typeof session.id === 'string' &&
session.lastActiveAt + SESSION_TTL > Date.now()
) {
currentSession = session as Session;
touchSession();
return currentSession;
}
} catch (error) {
console.error('Error parsing session', error);
@@ -65,7 +65,7 @@ export function touchSession() {
* Save the session to the local storage.
*/
export function saveSession() {
if (typeof localStorage !== 'undefined' && currentSession) {
localStorage.setItem(SESSION_KEY, JSON.stringify(currentSession));
if (currentSession) {
storage.setItem(SESSION_KEY, currentSession);
}
}
+36
View File
@@ -0,0 +1,36 @@
/**
* Get an item from local storage safely.
*/
export function getItem<T>(key: string, defaultValue: T): T {
if (typeof localStorage === 'undefined') {
return defaultValue;
}
try {
const stored = localStorage.getItem(key);
return stored ? (JSON.parse(stored) as T) : defaultValue;
} catch (error) {
if (error instanceof Error && error.name === 'SecurityError') {
return defaultValue;
}
throw error;
}
}
/**
* Set an item in local storage safely.
*/
export function setItem(key: string, value: unknown) {
if (typeof localStorage === 'undefined') {
return;
}
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
if (error instanceof Error && error.name === 'SecurityError') {
return;
}
throw error;
}
}