aboutsummaryrefslogtreecommitdiffstats
path: root/src/utils
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-10-26 19:07:31 +0200
committerArmand Philippot <git@armandphilippot.com>2023-11-11 18:15:27 +0100
commit795b92cc1a168c48c7710ca6e0e1ef5974013d95 (patch)
tree8f57204b0ffe7c8acb3203a24292f375377b6369 /src/utils
parent9aeb82269d7c74c4566b7ca254782a4dfbd69a6e (diff)
refactor(hooks): rewrite useLocalStorage hook
* return a tuple instead of an object * add a validator function as parameter (if the stored value is manually changed, it is not safe to cast its type) * add tests
Diffstat (limited to 'src/utils')
-rw-r--r--src/utils/hooks/use-local-storage.tsx33
-rw-r--r--src/utils/hooks/use-local-storage/index.ts1
-rw-r--r--src/utils/hooks/use-local-storage/use-local-storage.test.ts59
-rw-r--r--src/utils/hooks/use-local-storage/use-local-storage.ts38
-rw-r--r--src/utils/providers/prism-theme.tsx61
5 files changed, 133 insertions, 59 deletions
diff --git a/src/utils/hooks/use-local-storage.tsx b/src/utils/hooks/use-local-storage.tsx
deleted file mode 100644
index 0f9fbb6..0000000
--- a/src/utils/hooks/use-local-storage.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { LocalStorage } from '../../services/local-storage';
-import { Dispatch, SetStateAction, useEffect, useState } from 'react';
-
-export type UseLocalStorageReturn<T> = {
- value: T;
- setValue: Dispatch<SetStateAction<T>>;
-};
-
-/**
- * Use the local storage.
- *
- * @param {string} key - The storage local key.
- * @param {T} [fallbackValue] - A fallback value if local storage is empty.
- * @returns {UseLocalStorageReturn<T>} An object with value and setValue.
- */
-export const useLocalStorage = <T,>(
- key: string,
- fallbackValue: T
-): UseLocalStorageReturn<T> => {
- const getInitialValue = () => {
- if (typeof window === 'undefined') return fallbackValue;
- const storedValue = LocalStorage.get<T>(key);
- return storedValue ?? fallbackValue;
- };
-
- const [value, setValue] = useState<T>(getInitialValue);
-
- useEffect(() => {
- LocalStorage.set(key, value);
- }, [key, value]);
-
- return { value, setValue };
-};
diff --git a/src/utils/hooks/use-local-storage/index.ts b/src/utils/hooks/use-local-storage/index.ts
new file mode 100644
index 0000000..87c1953
--- /dev/null
+++ b/src/utils/hooks/use-local-storage/index.ts
@@ -0,0 +1 @@
+export * from './use-local-storage';
diff --git a/src/utils/hooks/use-local-storage/use-local-storage.test.ts b/src/utils/hooks/use-local-storage/use-local-storage.test.ts
new file mode 100644
index 0000000..3b63495
--- /dev/null
+++ b/src/utils/hooks/use-local-storage/use-local-storage.test.ts
@@ -0,0 +1,59 @@
+import { describe, expect, it } from '@jest/globals';
+import { act, renderHook } from '@testing-library/react';
+import { LocalStorage } from '../../../services/local-storage';
+import { useLocalStorage } from './use-local-storage';
+
+const validator = (value: unknown): value is string =>
+ typeof value === 'string';
+
+describe('useLocalStorage', () => {
+ const fallback = 'fuga';
+ const key = 'qui';
+
+ it('should return the fallback value when storage is clear', () => {
+ LocalStorage.clear();
+
+ const { result } = renderHook(() =>
+ useLocalStorage(key, fallback, validator)
+ );
+
+ expect(result.current[0]).toBe(fallback);
+ });
+
+ it('should return the stored value when storage is not clear', () => {
+ const storedValue = 'unde';
+
+ LocalStorage.set(key, storedValue);
+
+ const { result } = renderHook(() =>
+ useLocalStorage(key, fallback, validator)
+ );
+
+ expect(result.current[0]).toBe(storedValue);
+ });
+
+ it('should return the fallback value when the stored value is invalid', () => {
+ LocalStorage.clear();
+
+ const storedValue = 42;
+
+ LocalStorage.set(key, storedValue);
+
+ const { result } = renderHook(() =>
+ useLocalStorage(key, fallback, validator)
+ );
+
+ expect(result.current[0]).toBe(fallback);
+ });
+
+ it('can update the stored value', () => {
+ const { result } = renderHook(() =>
+ useLocalStorage(key, fallback, validator)
+ );
+ const newValue = 'eveniet';
+
+ act(() => result.current[1](newValue));
+
+ expect(result.current[0]).toBe(newValue);
+ });
+});
diff --git a/src/utils/hooks/use-local-storage/use-local-storage.ts b/src/utils/hooks/use-local-storage/use-local-storage.ts
new file mode 100644
index 0000000..47b98ff
--- /dev/null
+++ b/src/utils/hooks/use-local-storage/use-local-storage.ts
@@ -0,0 +1,38 @@
+import { useEffect, useState } from 'react';
+import { LocalStorage } from '../../../services/local-storage';
+import type { Validator } from '../../../types';
+
+const getInitialValueOrFallback = <T>(
+ key: string,
+ fallbackValue: T,
+ validator: Validator<T>
+) => {
+ if (typeof window === 'undefined') return fallbackValue;
+ const storedValue = LocalStorage.get(key);
+
+ return validator(storedValue) ? storedValue : fallbackValue;
+};
+
+/**
+ * Use the local storage.
+ *
+ * @param {string} key - The storage local key.
+ * @param {T} fallbackValue - A fallback value if local storage is empty.
+ * @param {Validator<T>} validator - A function to validate the stored value.
+ * @returns A tuple with the value and a setter.
+ */
+export const useLocalStorage = <T>(
+ key: string,
+ fallbackValue: T,
+ validator: Validator<T>
+) => {
+ const [value, setValue] = useState(
+ getInitialValueOrFallback(key, fallbackValue, validator)
+ );
+
+ useEffect(() => {
+ LocalStorage.set(key, value);
+ }, [key, value]);
+
+ return [value, setValue] as const;
+};
diff --git a/src/utils/providers/prism-theme.tsx b/src/utils/providers/prism-theme.tsx
index c063bd5..603e8a7 100644
--- a/src/utils/providers/prism-theme.tsx
+++ b/src/utils/providers/prism-theme.tsx
@@ -1,11 +1,13 @@
+/* eslint-disable max-statements */
import {
createContext,
- FC,
- ReactNode,
+ type FC,
+ type ReactNode,
useCallback,
useContext,
useEffect,
useState,
+ useMemo,
} from 'react';
import { useAttributes, useLocalStorage, useQuerySelectorAll } from '../hooks';
@@ -42,12 +44,9 @@ export const usePrismTheme = () => useContext(PrismThemeContext);
* @returns {boolean|undefined} True if `prefers-color-scheme` is set to `dark`.
*/
const prefersDarkScheme = (): boolean | undefined => {
- if (typeof window === 'undefined') return;
+ if (typeof window === 'undefined') return undefined;
- return (
- window.matchMedia &&
- window.matchMedia('(prefers-color-scheme: dark)').matches
- );
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
};
/**
@@ -56,25 +55,33 @@ const prefersDarkScheme = (): boolean | undefined => {
* @param {string} theme - A string.
* @returns {boolean} True if the given string match a Prism theme name.
*/
-const isValidTheme = (theme: string): boolean => {
- return theme === 'dark' || theme === 'light' || theme === 'system';
-};
+const isValidTheme = (theme: string): boolean =>
+ theme === 'dark' || theme === 'light' || theme === 'system';
+
+const defaultThemes = ['dark', 'light', 'system'] satisfies PrismTheme[];
+
+const validator = (value: unknown): value is PrismTheme =>
+ typeof value === 'string' && (defaultThemes as string[]).includes(value);
export const PrismThemeProvider: FC<PrismThemeProviderProps> = ({
attribute = 'data-prismjs-color-scheme-current',
storageKey = 'prismjs-color-scheme',
- themes = ['dark', 'light', 'system'],
+ themes = defaultThemes,
children,
}) => {
/**
* Retrieve the theme to use depending on `prefers-color-scheme`.
*/
const getThemeFromSystem = useCallback(() => {
- return prefersDarkScheme() ? 'dark' : 'light';
+ if (prefersDarkScheme()) return 'dark';
+ return 'light';
}, []);
- const { value: prismTheme, setValue: setPrismTheme } =
- useLocalStorage<PrismTheme>(storageKey, 'system');
+ const [prismTheme, setPrismTheme] = useLocalStorage<PrismTheme>(
+ storageKey,
+ 'system',
+ validator
+ );
useEffect(() => {
if (!isValidTheme(prismTheme)) setPrismTheme('system');
@@ -113,10 +120,10 @@ export const PrismThemeProvider: FC<PrismThemeProviderProps> = ({
*/
const listenAttributeChange = useCallback(
(pre: HTMLPreElement) => {
- var observer = new MutationObserver(function (mutations) {
+ const observer = new MutationObserver((mutations) => {
mutations.forEach((record) => {
- var mutatedPre = record.target as HTMLPreElement;
- var newTheme = mutatedPre.getAttribute(attribute) as PrismTheme;
+ const mutatedPre = record.target as HTMLPreElement;
+ const newTheme = mutatedPre.getAttribute(attribute) as PrismTheme;
setPrismTheme(newTheme);
});
});
@@ -133,16 +140,18 @@ export const PrismThemeProvider: FC<PrismThemeProviderProps> = ({
preTags.forEach(listenAttributeChange);
}, [preTags, listenAttributeChange]);
+ const value = useMemo(() => {
+ return {
+ themes,
+ theme: prismTheme,
+ setTheme: setPrismTheme,
+ codeBlocks: preTags,
+ resolvedTheme,
+ };
+ }, [preTags, prismTheme, resolvedTheme, setPrismTheme, themes]);
+
return (
- <PrismThemeContext.Provider
- value={{
- themes,
- theme: prismTheme,
- setTheme: setPrismTheme,
- codeBlocks: preTags,
- resolvedTheme,
- }}
- >
+ <PrismThemeContext.Provider value={value}>
{children}
</PrismThemeContext.Provider>
);