Skip to content

Instantly share code, notes, and snippets.

@JohnBra
Created July 4, 2022 22:44
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save JohnBra/c81451ea7bc9e77f8021beb4f198ab96 to your computer and use it in GitHub Desktop.
Save JohnBra/c81451ea7bc9e77f8021beb4f198ab96 to your computer and use it in GitHub Desktop.
Typed React useStorage hook for chrome extensions
import { Dispatch, SetStateAction, useState, useEffect, useCallback, useRef } from 'react';
export type StorageArea = 'sync' | 'local';
// custom hook to set chrome local/sync storage
// should also set a listener on this specific key
type SetValue<T> = Dispatch<SetStateAction<T>>;
/**
* Returns a stateful value from storage, and a function to update it.
*/
export function useStorage<T>(key: string, initialValue: T, area: StorageArea = 'local'): [T, SetValue<T>] {
const [storedValue, setStoredValue] = useState<T>(initialValue);
useEffect(() => {
readStorage<T>(key, area).then(res => {
if (res) setStoredValue(res);
});
chrome.storage.onChanged.addListener((changes, namespace) => {
if (namespace === area && changes.hasOwnProperty(key)) {
if (changes[key].newValue) setStoredValue(changes[key].newValue);
}
});
}, []);
const setValueRef = useRef<SetValue<T>>();
setValueRef.current = value => {
// Allow value to be a function, so we have the same API as useState
const newValue = value instanceof Function ? value(storedValue) : value;
// Save to storage
setStoredValue(prevState => {
setStorage<T>(key, newValue, area)
.then((success) => {
if (!success) setStoredValue(prevState);
});
return newValue;
});
}
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to storage.
const setValue: SetValue<T> = useCallback(
value => setValueRef.current?.(value),
[],
)
return [storedValue, setValue];
}
/**
* Retrieves value from chrome storage area
*
* @param key
* @param area - defaults to local
*/
export async function readStorage<T>(key: string, area: StorageArea = 'local'): Promise<T | undefined> {
try {
const result = await chrome.storage[area].get(key);
return result?.[key];
} catch (error) {
console.warn(`Error reading ${area} storage key "${key}":`, error);
return undefined;
}
}
/**
* Sets object in chrome storage area
*
* @param key
* @param value - value to be saved
* @param area - defaults to local
*/
export async function setStorage<T>(key: string, value: T, area: StorageArea = 'local'): Promise<boolean> {
try {
await chrome.storage[area].set({ [key]: value });
return true;
} catch (error) {
console.warn(`Error setting ${area} storage key "${key}":`, error);
return false;
}
}
@JohnBra
Copy link
Author

JohnBra commented Jul 5, 2022

A usage example is wrapping the hook again for settings in sync storage like so:

import { Dispatch, SetStateAction } from 'react';
import { useStorage } from './useStorage';

type Settings = {
  darkMode: boolean;
}

/**
 * Returns a stateful settings value from sync storage, and a function to update it.
 */
export function useSettings(): [Settings, Dispatch<SetStateAction<Settings>>] {
  return useStorage<Settings>('userSettings', 'sync');
}

From there you can just import it in any of your react pages. E.g. Options, Popup, etc.

@siman
Copy link

siman commented Apr 30, 2023

@JohnBra there is a TS error: Cannot find name 'chrome'

@noahvember
Copy link

@siman install @types/chrome

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment