diff --git a/README.md b/README.md
index 1bec540..c7a83f4 100644
--- a/README.md
+++ b/README.md
@@ -23,13 +23,14 @@ Compatible with React v18.0.0+.
- [useDocumentTitle](https://usehooks.com/usedocumenttitle)
- [useFavicon](https://usehooks.com/usefavicon)
- [useGeolocation](https://usehooks.com/usegeolocation)
-- [useHistoryState](https://usehooks.com/usehistoryState)
+- [useHistoryState](https://usehooks.com/usehistorystate)
- [useHover](https://usehooks.com/usehover)
- [useIdle](https://usehooks.com/useidle)
- [useIntersectionObserver](https://usehooks.com/useintersectionobserver)
- [useIsClient](https://usehooks.com/useisclient)
- [useIsFirstRender](https://usehooks.com/useisfirstrender)
- [useList](https://usehooks.com/uselist)
+- [useLocalStorage](https://usehooks.com/uselocalstorage)
- [useLockBodyScroll](https://usehooks.com/uselockbodyscroll)
- [useLongPress](https://usehooks.com/uselongpress)
- [useMap](https://usehooks.com/usemap)
@@ -45,6 +46,7 @@ Compatible with React v18.0.0+.
- [useRenderCount](https://usehooks.com/userendercount)
- [useRenderInfo](https://usehooks.com/userenderinfo)
- [useScript](https://usehooks.com/usescript)
+- [useSessionStorage](https://usehooks.com/usesessionstorage)
- [useSet](https://usehooks.com/useset)
- [useThrottle](https://usehooks.com/usethrottle)
- [useToggle](https://usehooks.com/usetoggle)
@@ -67,9 +69,7 @@ Compatible with React v18.0.0+.
- [useInterval](https://usehooks.com/useinterval)
- [useIntervalWhen](https://usehooks.com/useintervalwhen)
- [useKeyPress](https://usehooks.com/usekeypress)
-- [useLocalStorage](https://usehooks.com/uselocalstorage)
- [useLogger](https://usehooks.com/uselogger)
- [usePageLeave](https://usehooks.com/usepageleave)
- [useRandomInterval](https://usehooks.com/userandominterval)
-- [useSessionStorage](https://usehooks.com/usesessionstorage)
- [useTimeout](https://usehooks.com/usetimeout)
diff --git a/index.d.ts b/index.d.ts
index 8e27f47..db01dc2 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -82,6 +82,7 @@ export type CustomQueue = {
first: T | undefined;
last: T | undefined;
size: number;
+ queue: T[];
};
export type RenderInfo = {
@@ -123,10 +124,7 @@ declare module "@uidotdev/usehooks" {
): React.MutableRefObject;
export function useCopyToClipboard(): [
- {
- error: Error | null;
- text: string | null;
- },
+ string | null,
(value: string) => Promise
];
@@ -162,15 +160,15 @@ declare module "@uidotdev/usehooks" {
export function useHistoryState(initialPresent?: T): HistoryState;
export function useHover(): [
- React.MutableRefObject,
+ React.RefCallback,
boolean
];
export function useIdle(ms?: number): boolean;
- export function useIntersectionObserver(
+ export function useIntersectionObserver(
options?: IntersectionObserverInit
- ): [React.MutableRefObject, IntersectionObserverEntry | null];
+ ): [React.RefCallback, IntersectionObserverEntry | null];
export function useIsClient(): boolean;
@@ -178,6 +176,11 @@ declare module "@uidotdev/usehooks" {
export function useList(defaultList?: T[]): [T[], CustomList];
+ export function useLocalStorage(
+ key: string,
+ initialValue?: T
+ ): [T, React.Dispatch>];
+
export function useLockBodyScroll(): void;
export function useLongPress(
@@ -188,7 +191,7 @@ declare module "@uidotdev/usehooks" {
export function useMap(initialState?: T): Map;
export function useMeasure(): [
- React.MutableRefObject,
+ React.RefCallback,
{
width: number | null;
height: number | null;
@@ -226,7 +229,12 @@ declare module "@uidotdev/usehooks" {
options?: {
removeOnUnmount?: boolean;
}
- ): "idle" | "loading" | "ready" | "error";
+ ): "unknown" | "loading" | "ready" | "error";
+
+ export function useSessionStorage(
+ key: string,
+ initialValue: T
+ ): [T, React.Dispatch>];
export function useSet(values?: T[]): Set;
diff --git a/index.js b/index.js
index ef502e4..f6e4fe2 100644
--- a/index.js
+++ b/index.js
@@ -38,6 +38,14 @@ function throttle(cb, ms) {
};
}
+function isPlainObject(value) {
+ return Object.prototype.toString.call(value) === "[object Object]";
+}
+
+function dispatchStorageEvent(key, newValue) {
+ window.dispatchEvent(new StorageEvent("storage", { key, newValue }));
+}
+
export function useBattery() {
const [state, setState] = React.useState({
supported: true,
@@ -122,35 +130,34 @@ export function useClickAway(cb) {
return ref;
}
-export function useCopyToClipboard() {
- const [state, setState] = React.useState({
- error: null,
- text: null,
- });
-
- const copyToClipboard = React.useCallback(async (value) => {
- if (!navigator?.clipboard) {
- return setState({
- error: new Error("Clipboard not supported"),
- text: null,
- });
- }
-
- const handleSuccess = () => {
- setState({
- error: null,
- text: value,
- });
- };
+function oldSchoolCopy(text) {
+ const tempTextArea = document.createElement("textarea");
+ tempTextArea.value = text;
+ document.body.appendChild(tempTextArea);
+ tempTextArea.select();
+ document.execCommand("copy");
+ document.body.removeChild(tempTextArea);
+}
- const handleFailure = (e) => {
- setState({
- error: e,
- text: null,
- });
+export function useCopyToClipboard() {
+ const [state, setState] = React.useState(null);
+
+ const copyToClipboard = React.useCallback((value) => {
+ const handleCopy = async () => {
+ try {
+ if (navigator?.clipboard?.writeText) {
+ await navigator.clipboard.writeText(value);
+ setState(value);
+ } else {
+ throw new Error("writeText not supported");
+ }
+ } catch (e) {
+ oldSchoolCopy(value);
+ setState(value);
+ }
};
- navigator.clipboard.writeText(value).then(handleSuccess, handleFailure);
+ handleCopy();
}, []);
return [state, copyToClipboard];
@@ -159,13 +166,13 @@ export function useCopyToClipboard() {
export function useCounter(startingValue = 0, options = {}) {
const { min, max } = options;
- if (min && startingValue < min) {
+ if (typeof min === "number" && startingValue < min) {
throw new Error(
`Your starting value of ${startingValue} is less than your min of ${min}.`
);
}
- if (max && startingValue > max) {
+ if (typeof max === "number" && startingValue > max) {
throw new Error(
`Your starting value of ${startingValue} is greater than your max of ${max}.`
);
@@ -173,47 +180,50 @@ export function useCounter(startingValue = 0, options = {}) {
const [count, setCount] = React.useState(startingValue);
- const increment = () => {
- const nextCount = count + 1;
- if (max && nextCount > max) {
- return;
- }
+ const increment = React.useCallback(() => {
+ setCount((c) => {
+ const nextCount = c + 1;
- setCount(nextCount);
- };
+ if (typeof max === "number" && nextCount > max) {
+ return c;
+ }
- const decrement = () => {
- const nextCount = count - 1;
- if (min && nextCount < min) {
- return;
- }
+ return nextCount;
+ });
+ }, [max]);
- setCount(nextCount);
- };
+ const decrement = React.useCallback(() => {
+ setCount((c) => {
+ const nextCount = c - 1;
- const set = (nextCount) => {
- if (max && nextCount > max) {
- return;
- }
+ if (typeof min === "number" && nextCount < min) {
+ return c;
+ }
- if (min && nextCount < min) {
- return;
- }
+ return nextCount;
+ });
+ }, [min]);
- if (nextCount === count) {
- return;
- }
+ const set = React.useCallback(
+ (nextCount) => {
+ setCount((c) => {
+ if (typeof max === "number" && nextCount > max) {
+ return c;
+ }
- setCount(nextCount);
- };
+ if (typeof min === "number" && nextCount < min) {
+ return c;
+ }
- const reset = () => {
- if (count === startingValue) {
- return;
- }
+ return nextCount;
+ });
+ },
+ [max, min]
+ );
+ const reset = React.useCallback(() => {
setCount(startingValue);
- };
+ }, [startingValue]);
return [
count,
@@ -260,13 +270,17 @@ export function useDocumentTitle(title) {
export function useFavicon(url) {
React.useEffect(() => {
- const link =
- document.querySelector("link[rel*='icon']") ||
- document.createElement("link");
- link.type = "image/x-icon";
- link.rel = "shortcut icon";
- link.href = url;
- document.getElementsByTagName("head")[0].appendChild(link);
+ let link = document.querySelector(`link[rel~="icon"]`);
+
+ if (!link) {
+ link = document.createElement("link");
+ link.type = "image/x-icon";
+ link.rel = "icon";
+ link.href = url;
+ document.head.appendChild(link);
+ } else {
+ link.href = url;
+ }
}, [url]);
}
@@ -337,38 +351,38 @@ const initialUseHistoryStateState = {
const useHistoryStateReducer = (state, action) => {
const { past, present, future } = state;
- switch (action.type) {
- case "UNDO":
- return {
- past: past.slice(0, past.length - 1),
- present: past[past.length - 1],
- future: [present, ...future],
- };
- case "REDO":
- return {
- past: [...past, present],
- present: future[0],
- future: future.slice(1),
- };
- case "SET":
- const { newPresent } = action;
- if (action.newPresent === present) {
- return state;
- }
+ if (action.type === "UNDO") {
+ return {
+ past: past.slice(0, past.length - 1),
+ present: past[past.length - 1],
+ future: [present, ...future],
+ };
+ } else if (action.type === "REDO") {
+ return {
+ past: [...past, present],
+ present: future[0],
+ future: future.slice(1),
+ };
+ } else if (action.type === "SET") {
+ const { newPresent } = action;
- return {
- past: [...past, present],
- present: newPresent,
- future: [],
- };
- case "CLEAR":
- return {
- ...initialUseHistoryStateState,
- present: action.initialPresent,
- };
- default:
- throw new Error("Unsupported action type");
+ if (action.newPresent === present) {
+ return state;
+ }
+
+ return {
+ past: [...past, present],
+ present: newPresent,
+ future: [],
+ };
+ } else if (action.type === "CLEAR") {
+ return {
+ ...initialUseHistoryStateState,
+ present: action.initialPresent,
+ };
+ } else {
+ throw new Error("Unsupported action type");
}
};
@@ -411,31 +425,40 @@ export function useHistoryState(initialPresent = {}) {
export function useHover() {
const [hovering, setHovering] = React.useState(false);
- const ref = React.useRef(null);
-
- React.useEffect(() => {
- const node = ref.current;
+ const previousNode = React.useRef(null);
- if (!node) return;
+ const handleMouseEnter = React.useCallback(() => {
+ setHovering(true);
+ }, []);
- const handleMouseEnter = () => {
- setHovering(true);
- };
+ const handleMouseLeave = React.useCallback(() => {
+ setHovering(false);
+ }, []);
- const handleMouseLeave = () => {
- setHovering(false);
- };
+ const customRef = React.useCallback(
+ (node) => {
+ if (previousNode.current?.nodeType === Node.ELEMENT_NODE) {
+ previousNode.current.removeEventListener(
+ "mouseenter",
+ handleMouseEnter
+ );
+ previousNode.current.removeEventListener(
+ "mouseleave",
+ handleMouseLeave
+ );
+ }
- node.addEventListener("mouseenter", handleMouseEnter);
- node.addEventListener("mouseleave", handleMouseLeave);
+ if (node?.nodeType === Node.ELEMENT_NODE) {
+ node.addEventListener("mouseenter", handleMouseEnter);
+ node.addEventListener("mouseleave", handleMouseLeave);
+ }
- return () => {
- node.removeEventListener("mouseenter", handleMouseEnter);
- node.removeEventListener("mouseleave", handleMouseLeave);
- };
- }, []);
+ previousNode.current = node;
+ },
+ [handleMouseEnter, handleMouseLeave]
+ );
- return [ref, hovering];
+ return [customRef, hovering];
}
export function useIdle(ms = 1000 * 60) {
@@ -487,33 +510,34 @@ export function useIdle(ms = 1000 * 60) {
}
export function useIntersectionObserver(options = {}) {
- const { threshold = 1, root = null, rootMargin = "0%" } = options;
- const ref = React.useRef(null);
+ const { threshold = 1, root = null, rootMargin = "0px" } = options;
const [entry, setEntry] = React.useState(null);
- React.useEffect(() => {
- const node = ref?.current;
-
- if (!node || typeof IntersectionObserver !== "function") {
- return;
- }
+ const previousObserver = React.useRef(null);
- const observer = new IntersectionObserver(
- ([entry]) => {
- setEntry(entry);
- },
- { threshold, root, rootMargin }
- );
+ const customRef = React.useCallback(
+ (node) => {
+ if (previousObserver.current) {
+ previousObserver.current.disconnect();
+ previousObserver.current = null;
+ }
- observer.observe(node);
+ if (node?.nodeType === Node.ELEMENT_NODE) {
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ setEntry(entry);
+ },
+ { threshold, root, rootMargin }
+ );
- return () => {
- setEntry(null);
- observer.disconnect();
- };
- }, [threshold, root, rootMargin]);
+ observer.observe(node);
+ previousObserver.current = observer;
+ }
+ },
+ [threshold, root, rootMargin]
+ );
- return [ref, entry];
+ return [customRef, entry];
}
export function useIsClient() {
@@ -540,44 +564,95 @@ export function useIsFirstRender() {
export function useList(defaultList = []) {
const [list, setList] = React.useState(defaultList);
- const methods = React.useMemo(() => {
- const set = (l) => {
- setList(l);
- };
+ const set = React.useCallback((l) => {
+ setList(l);
+ }, []);
- const push = (element) => {
- setList((l) => [...l, element]);
- };
+ const push = React.useCallback((element) => {
+ setList((l) => [...l, element]);
+ }, []);
- const removeAt = (index) => {
- setList((l) => [...l.slice(0, index), ...l.slice(index + 1)]);
- };
+ const removeAt = React.useCallback((index) => {
+ setList((l) => [...l.slice(0, index), ...l.slice(index + 1)]);
+ }, []);
- const insertAt = (index, element) => {
- setList((l) => [...l.slice(0, index), element, ...l.slice(index)]);
- };
+ const insertAt = React.useCallback((index, element) => {
+ setList((l) => [...l.slice(0, index), element, ...l.slice(index)]);
+ }, []);
- const updateAt = (index, element) => {
- setList((l) => l.map((e, i) => (i === index ? element : e)));
- };
+ const updateAt = React.useCallback((index, element) => {
+ setList((l) => l.map((e, i) => (i === index ? element : e)));
+ }, []);
- const clear = () => setList([]);
+ const clear = React.useCallback(() => setList([]), []);
- return {
- set,
- push,
- removeAt,
- insertAt,
- updateAt,
- clear,
- };
- }, []);
+ return [list, { set, push, removeAt, insertAt, updateAt, clear }];
+}
+
+const setLocalStorageItem = (key, value) => {
+ const stringifiedValue = JSON.stringify(value);
+ window.localStorage.setItem(key, stringifiedValue);
+ dispatchStorageEvent(key, stringifiedValue);
+};
+
+const removeLocalStorageItem = (key) => {
+ window.localStorage.removeItem(key);
+ dispatchStorageEvent(key, null);
+};
+
+const getLocalStorageItem = (key) => {
+ return window.localStorage.getItem(key);
+};
+
+const useLocalStorageSubscribe = (callback) => {
+ window.addEventListener("storage", callback);
+ return () => window.removeEventListener("storage", callback);
+};
- return [list, methods];
+const getLocalStorageServerSnapshot = () => {
+ throw Error("useLocalStorage is a client-only hook");
+};
+
+export function useLocalStorage(key, initialValue) {
+ const getSnapshot = () => getLocalStorageItem(key);
+
+ const store = React.useSyncExternalStore(
+ useLocalStorageSubscribe,
+ getSnapshot,
+ getLocalStorageServerSnapshot
+ );
+
+ const setState = React.useCallback(
+ (v) => {
+ try {
+ const nextState = typeof v === "function" ? v(JSON.parse(store)) : v;
+
+ if (nextState === undefined || nextState === null) {
+ removeLocalStorageItem(key);
+ } else {
+ setLocalStorageItem(key, nextState);
+ }
+ } catch (e) {
+ console.warn(e);
+ }
+ },
+ [key, store]
+ );
+
+ React.useEffect(() => {
+ if (
+ getLocalStorageItem(key) === null &&
+ typeof initialValue !== "undefined"
+ ) {
+ setLocalStorageItem(key, initialValue);
+ }
+ }, [key, initialValue]);
+
+ return [store ? JSON.parse(store) : initialValue, setState];
}
export function useLockBodyScroll() {
- React.useEffect(() => {
+ React.useLayoutEffect(() => {
const originalStyle = window.getComputedStyle(document.body).overflow;
document.body.style.overflow = "hidden";
return () => {
@@ -586,23 +661,18 @@ export function useLockBodyScroll() {
}, []);
}
-export function useLongPress(
- callback,
- { threshold = 400, onStart, onFinish, onCancel } = {}
-) {
+export function useLongPress(callback, options = {}) {
+ const { threshold = 400, onStart, onFinish, onCancel } = options;
const isLongPressActive = React.useRef(false);
const isPressed = React.useRef(false);
const timerId = React.useRef();
- const cbRef = React.useRef(callback);
- React.useLayoutEffect(() => {
- cbRef.current = callback;
- });
-
- const start = React.useCallback(
- () => (event) => {
- if (isPressed.current) return;
+ return React.useMemo(() => {
+ if (typeof callback !== "function") {
+ return {};
+ }
+ const start = (event) => {
if (!isMouseEvent(event) && !isTouchEvent(event)) return;
if (onStart) {
@@ -611,15 +681,12 @@ export function useLongPress(
isPressed.current = true;
timerId.current = setTimeout(() => {
- cbRef.current(event);
+ callback(event);
isLongPressActive.current = true;
}, threshold);
- },
- [onStart, threshold]
- );
+ };
- const cancel = React.useCallback(
- () => (event) => {
+ const cancel = (event) => {
if (!isMouseEvent(event) && !isTouchEvent(event)) return;
if (isLongPressActive.current) {
@@ -638,31 +705,24 @@ export function useLongPress(
if (timerId.current) {
window.clearTimeout(timerId.current);
}
- },
- [onFinish, onCancel]
- );
-
- return React.useMemo(() => {
- if (callback === null) {
- return {};
- }
+ };
const mouseHandlers = {
- onMouseDown: start(),
- onMouseUp: cancel(),
- onMouseLeave: cancel(),
+ onMouseDown: start,
+ onMouseUp: cancel,
+ onMouseLeave: cancel,
};
const touchHandlers = {
- onTouchStart: start(),
- onTouchEnd: cancel(),
+ onTouchStart: start,
+ onTouchEnd: cancel,
};
return {
...mouseHandlers,
...touchHandlers,
};
- }, [callback, cancel, start]);
+ }, [callback, threshold, onCancel, onFinish, onStart]);
}
export function useMap(initialState) {
@@ -691,31 +751,35 @@ export function useMap(initialState) {
}
export function useMeasure() {
- const ref = React.useRef(null);
- const [rect, setRect] = React.useState({
+ const [dimensions, setDimensions] = React.useState({
width: null,
height: null,
});
- React.useLayoutEffect(() => {
- if (!ref.current) return;
-
- const observer = new ResizeObserver(([entry]) => {
- if (entry && entry.contentRect) {
- setRect({
- width: entry.contentRect.width,
- height: entry.contentRect.height,
- });
- }
- });
+ const previousObserver = React.useRef(null);
- observer.observe(ref.current);
- return () => {
- observer.disconnect();
- };
+ const customRef = React.useCallback((node) => {
+ if (previousObserver.current) {
+ previousObserver.current.disconnect();
+ previousObserver.current = null;
+ }
+
+ if (node?.nodeType === Node.ELEMENT_NODE) {
+ const observer = new ResizeObserver(([entry]) => {
+ if (entry && entry.borderBoxSize) {
+ const { inlineSize: width, blockSize: height } =
+ entry.borderBoxSize[0];
+
+ setDimensions({ width, height });
+ }
+ });
+
+ observer.observe(node);
+ previousObserver.current = observer;
+ }
}, []);
- return [ref, rect];
+ return [customRef, dimensions];
}
export function useMediaQuery(query) {
@@ -761,15 +825,13 @@ export function useMouse() {
y: event.pageY,
};
- if (ref.current instanceof HTMLElement) {
+ if (ref.current?.nodeType === Node.ELEMENT_NODE) {
const { left, top } = ref.current.getBoundingClientRect();
- const elementPositionX = left + window.pageXOffset;
- const elementPositionY = top + window.pageYOffset;
+ const elementPositionX = left + window.scrollX;
+ const elementPositionY = top + window.scrollY;
const elementX = event.pageX - elementPositionX;
const elementY = event.pageY - elementPositionY;
- newState.elementX = elementX;
- newState.elementY = elementY;
newState.elementX = elementX;
newState.elementY = elementY;
newState.elementPositionX = elementPositionX;
@@ -794,34 +856,44 @@ export function useMouse() {
return [state, ref];
}
-export function useNetworkState() {
- const connection =
+const getConnection = () => {
+ return (
navigator?.connection ||
navigator?.mozConnection ||
- navigator?.webkitConnection;
+ navigator?.webkitConnection
+ );
+};
- const cache = React.useRef({});
+const useNetworkStateSubscribe = (callback) => {
+ window.addEventListener("online", callback, { passive: true });
+ window.addEventListener("offline", callback, { passive: true });
+
+ const connection = getConnection();
+
+ if (connection) {
+ connection.addEventListener("change", callback, { passive: true });
+ }
- const subscribe = React.useCallback((callback) => {
- window.addEventListener("online", callback, { passive: true });
- window.addEventListener("offline", callback, { passive: true });
+ return () => {
+ window.removeEventListener("online", callback);
+ window.removeEventListener("offline", callback);
if (connection) {
- connection.addEventListener("change", callback, { passive: true });
+ connection.removeEventListener("change", callback);
}
+ };
+};
- return () => {
- window.removeEventListener("online", callback);
- window.removeEventListener("offline", callback);
+const getNetworkStateServerSnapshot = () => {
+ throw Error("useNetworkState is a client-only hook");
+};
- if (connection) {
- connection.removeEventListener("change", callback);
- }
- };
- }, []);
+export function useNetworkState() {
+ const cache = React.useRef({});
const getSnapshot = () => {
const online = navigator.onLine;
+ const connection = getConnection();
const nextState = {
online,
@@ -841,11 +913,11 @@ export function useNetworkState() {
}
};
- const getServerSnapshot = () => {
- throw Error("useNetworkState is a client-only hook");
- };
-
- return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
+ return React.useSyncExternalStore(
+ useNetworkStateSubscribe,
+ getSnapshot,
+ getNetworkStateServerSnapshot
+ );
}
export function useObjectState(initialValue) {
@@ -856,14 +928,16 @@ export function useObjectState(initialValue) {
setState((s) => {
const newState = arg(s);
- return {
- ...s,
- ...newState,
- };
+ if (isPlainObject(newState)) {
+ return {
+ ...s,
+ ...newState,
+ };
+ }
});
}
- if (typeof arg === "object") {
+ if (isPlainObject(arg)) {
setState((s) => ({
...s,
...arg,
@@ -940,14 +1014,16 @@ export function usePreferredLanguage() {
);
}
-export function usePrevious(newValue) {
- const previousRef = React.useRef();
+export function usePrevious(value) {
+ const [current, setCurrent] = React.useState(value);
+ const [previous, setPrevious] = React.useState(null);
- React.useEffect(() => {
- previousRef.current = newValue;
- });
+ if (value !== current) {
+ setPrevious(current);
+ setCurrent(value);
+ }
- return previousRef.current;
+ return previous;
}
export function useQueue(initialValue = []) {
@@ -1019,74 +1095,121 @@ export function useRenderInfo(name = "Unknown") {
}
export function useScript(src, options = {}) {
- const [status, setStatus] = React.useState(() => {
- if (!src) {
- return "idle";
- }
-
- return "loading";
- });
-
- const cachedScriptStatuses = React.useRef({});
+ const [status, setStatus] = React.useState("loading");
+ const optionsRef = React.useRef(options);
React.useEffect(() => {
- if (!src) {
- return;
- }
+ let script = document.querySelector(`script[src="${src}"]`);
- const cachedScriptStatus = cachedScriptStatuses.current[src];
- if (cachedScriptStatus === "ready" || cachedScriptStatus === "error") {
- setStatus(cachedScriptStatus);
+ const domStatus = script?.getAttribute("data-status");
+ if (domStatus) {
+ setStatus(domStatus);
return;
}
- let script = document.querySelector(`script[src="${src}"]`);
-
- if (script) {
- setStatus(
- script.getAttribute("data-status") ?? cachedScriptStatus ?? "loading"
- );
- } else {
+ if (script === null) {
script = document.createElement("script");
script.src = src;
script.async = true;
script.setAttribute("data-status", "loading");
document.body.appendChild(script);
- const setAttributeFromEvent = (event) => {
- const scriptStatus = event.type === "load" ? "ready" : "error";
+ const handleScriptLoad = () => {
+ script.setAttribute("data-status", "ready");
+ setStatus("ready");
+ removeEventListeners();
+ };
- if (script) {
- script.setAttribute("data-status", scriptStatus);
- }
+ const handleScriptError = () => {
+ script.setAttribute("data-status", "error");
+ setStatus("error");
+ removeEventListeners();
+ };
+
+ const removeEventListeners = () => {
+ script.removeEventListener("load", handleScriptLoad);
+ script.removeEventListener("error", handleScriptError);
};
- script.addEventListener("load", setAttributeFromEvent);
- script.addEventListener("error", setAttributeFromEvent);
+ script.addEventListener("load", handleScriptLoad);
+ script.addEventListener("error", handleScriptError);
+
+ const removeOnUnmount = optionsRef.current.removeOnUnmount;
+
+ return () => {
+ if (removeOnUnmount === true) {
+ script.remove();
+ removeEventListeners();
+ }
+ };
+ } else {
+ setStatus("unknown");
}
+ }, [src]);
- const setStateFromEvent = (event) => {
- const newStatus = event.type === "load" ? "ready" : "error";
- setStatus(newStatus);
- cachedScriptStatuses.current[src] = newStatus;
- };
+ return status;
+}
- script.addEventListener("load", setStateFromEvent);
- script.addEventListener("error", setStateFromEvent);
+const setSessionStorageItem = (key, value) => {
+ const stringifiedValue = JSON.stringify(value);
+ window.sessionStorage.setItem(key, stringifiedValue);
+ dispatchStorageEvent(key, stringifiedValue);
+};
- return () => {
- if (script) {
- script.removeEventListener("load", setStateFromEvent);
- script.removeEventListener("error", setStateFromEvent);
- }
+const removeSessionStorageItem = (key) => {
+ window.sessionStorage.removeItem(key);
+ dispatchStorageEvent(key, null);
+};
+
+const getSessionStorageItem = (key) => {
+ return window.sessionStorage.getItem(key);
+};
+
+const useSessionStorageSubscribe = (callback) => {
+ window.addEventListener("storage", callback);
+ return () => window.removeEventListener("storage", callback);
+};
+
+const getSessionStorageServerSnapshot = () => {
+ throw Error("useSessionStorage is a client-only hook");
+};
+
+export function useSessionStorage(key, initialValue) {
+ const getSnapshot = () => getSessionStorageItem(key);
+
+ const store = React.useSyncExternalStore(
+ useSessionStorageSubscribe,
+ getSnapshot,
+ getSessionStorageServerSnapshot
+ );
+
+ const setState = React.useCallback(
+ (v) => {
+ try {
+ const nextState = typeof v === "function" ? v(JSON.parse(store)) : v;
- if (script && options.removeOnUnmount) {
- script.remove();
+ if (nextState === undefined || nextState === null) {
+ removeSessionStorageItem(key);
+ } else {
+ setSessionStorageItem(key, nextState);
+ }
+ } catch (e) {
+ console.warn(e);
}
- };
- }, [src, options.removeOnUnmount]);
+ },
+ [key, store]
+ );
- return status;
+ React.useEffect(() => {
+ if (
+ getSessionStorageItem(key) === null &&
+ typeof initialValue !== "undefined"
+ ) {
+ setSessionStorageItem(key, initialValue);
+ }
+ }, [key, initialValue]);
+
+ return [store ? JSON.parse(store) : initialValue, setState];
}
export function useSet(values) {
@@ -1115,65 +1238,14 @@ export function useSet(values) {
return setRef.current;
}
-export function useSpeech(text, options) {
- const [state, setState] = React.useState(() => {
- const { lang = "default", name = "" } = options.voice || {};
- return {
- isPlaying: false,
- status: "init",
- lang: options.lang || "default",
- voiceInfo: { lang, name },
- rate: options.rate || 1,
- pitch: options.pitch || 1,
- volume: options.volume || 1,
- };
- });
-
- const optionsRef = React.useRef(options);
-
- React.useEffect(() => {
- const handlePlay = () => {
- setState((s) => {
- return { ...s, isPlaying: true, status: "play" };
- });
- };
-
- const handlePause = () => {
- setState((s) => {
- return { ...s, isPlaying: false, status: "pause" };
- });
- };
-
- const handleEnd = () => {
- setState((s) => {
- return { ...s, isPlaying: false, status: "end" };
- });
- };
-
- const utterance = new SpeechSynthesisUtterance(text);
- optionsRef.current.lang && (utterance.lang = optionsRef.current.lang);
- optionsRef.current.voice && (utterance.voice = optionsRef.current.voice);
- utterance.rate = optionsRef.current.rate || 1;
- utterance.pitch = optionsRef.current.pitch || 1;
- utterance.volume = optionsRef.current.volume || 1;
- utterance.onstart = handlePlay;
- utterance.onpause = handlePause;
- utterance.onresume = handlePlay;
- utterance.onend = handleEnd;
- window.speechSynthesis.speak(utterance);
- }, [text]);
-
- return state;
-}
-
export function useThrottle(value, interval = 500) {
const [throttledValue, setThrottledValue] = React.useState(value);
- const lastUpdated = React.useRef();
+ const lastUpdated = React.useRef(null);
React.useEffect(() => {
const now = Date.now();
- if (now >= lastUpdated.current + interval) {
+ if (lastUpdated.current && now >= lastUpdated.current + interval) {
lastUpdated.current = now;
setThrottledValue(value);
} else {
@@ -1190,7 +1262,13 @@ export function useThrottle(value, interval = 500) {
}
export function useToggle(initialValue) {
- const [on, setOn] = React.useState(initialValue);
+ const [on, setOn] = React.useState(() => {
+ if (typeof initialValue === "boolean") {
+ return initialValue;
+ }
+
+ return Boolean(initialValue);
+ });
const handleToggle = React.useCallback((value) => {
if (typeof value === "boolean") {
@@ -1203,27 +1281,30 @@ export function useToggle(initialValue) {
return [on, handleToggle];
}
-export function useVisibilityChange() {
- const [documentVisible, setDocumentVisibility] = React.useState(true);
+const useVisibilityChangeSubscribe = (callback) => {
+ document.addEventListener("visibilitychange", callback);
- React.useEffect(() => {
- const handleChange = () => {
- if (document.visibilityState !== "visible") {
- setDocumentVisibility(false);
- } else {
- setDocumentVisibility(true);
- }
- };
- handleChange();
+ return () => {
+ document.removeEventListener("visibilitychange", callback);
+ };
+};
- document.addEventListener("visibilitychange", handleChange);
+const getVisibilityChangeSnapshot = () => {
+ return document.visibilityState;
+};
- return () => {
- document.removeEventListener("visibilitychange", handleChange);
- };
- }, []);
+const getVisibilityChangeServerSnapshot = () => {
+ throw Error("useVisibilityChange is a client-only hook");
+};
+
+export function useVisibilityChange() {
+ const visibilityState = React.useSyncExternalStore(
+ useVisibilityChangeSubscribe,
+ getVisibilityChangeSnapshot,
+ getVisibilityChangeServerSnapshot
+ );
- return documentVisible;
+ return visibilityState === "visible";
}
export function useWindowScroll() {
@@ -1246,7 +1327,7 @@ export function useWindowScroll() {
React.useLayoutEffect(() => {
const handleScroll = () => {
- setState({ x: window.pageXOffset, y: window.pageYOffset });
+ setState({ x: window.scrollX, y: window.scrollY });
};
handleScroll();
diff --git a/package-lock.json b/package-lock.json
index 48532c0..d920bd2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@uidotdev/usehooks",
- "version": "2.1.0",
+ "version": "2.4.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@uidotdev/usehooks",
- "version": "2.1.0",
+ "version": "2.4.1",
"license": "MIT",
"devDependencies": {
"@types/react": "^18.2.20",
diff --git a/package.json b/package.json
index e33034d..12908aa 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@uidotdev/usehooks",
- "version": "2.1.0",
+ "version": "2.4.1",
"description": "A collection of modern, server-safe React hooks – from the ui.dev team",
"type": "module",
"repository": "uidotdev/usehooks",
diff --git a/usehooks.com/public/img/banner-sale-reactgg.svg b/usehooks.com/public/img/banner-sale-reactgg.svg
new file mode 100644
index 0000000..9aee0dd
--- /dev/null
+++ b/usehooks.com/public/img/banner-sale-reactgg.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/usehooks.com/public/img/bytes-tshirt.png b/usehooks.com/public/img/bytes-tshirt.png
new file mode 100644
index 0000000..5f59d9c
Binary files /dev/null and b/usehooks.com/public/img/bytes-tshirt.png differ
diff --git a/usehooks.com/public/img/react-gg-logo-sticker.svg b/usehooks.com/public/img/react-gg-logo-sticker.svg
new file mode 100644
index 0000000..3e6f58a
--- /dev/null
+++ b/usehooks.com/public/img/react-gg-logo-sticker.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/usehooks.com/src/components/CountdownTimer.tsx b/usehooks.com/src/components/CountdownTimer.tsx
new file mode 100644
index 0000000..72ea4d4
--- /dev/null
+++ b/usehooks.com/src/components/CountdownTimer.tsx
@@ -0,0 +1,89 @@
+import { Fragment, useEffect, useState } from "react";
+
+interface CountdownProps {
+ targetDate: string; // YYYY-MM-DD format
+}
+
+interface TimeLeft {
+ days: number;
+ hours: number;
+ minutes: number;
+ seconds: number;
+}
+
+function calculateTimeLeft(targetDate: string): TimeLeft {
+ const target = new Date(`${targetDate}T00:00:00-08:00`);
+ const now = new Date();
+ const difference = +target - +now;
+
+ if (difference <= 0) {
+ return {
+ days: 0,
+ hours: 0,
+ minutes: 0,
+ seconds: 0,
+ };
+ }
+
+ return {
+ days: Math.floor(difference / (1000 * 60 * 60 * 24)),
+ hours: Math.floor((difference / (1000 * 60 * 60)) % 24),
+ minutes: Math.floor((difference / 1000 / 60) % 60),
+ seconds: Math.floor((difference / 1000) % 60),
+ };
+}
+
+const formatNumber = (number: number) => number.toString().padStart(2, "0");
+
+const Countdown: React.FC = ({ targetDate }) => {
+ const [timeLeft, setTimeLeft] = useState(
+ calculateTimeLeft(targetDate)
+ );
+
+ useEffect(() => {
+ const timer = setInterval(() => {
+ const newTimeLeft = calculateTimeLeft(targetDate);
+ setTimeLeft(newTimeLeft);
+ if (
+ newTimeLeft.days === 0 &&
+ newTimeLeft.hours === 0 &&
+ newTimeLeft.minutes === 0 &&
+ newTimeLeft.seconds === 0
+ ) {
+ clearInterval(timer);
+ }
+ }, 1000);
+
+ return () => clearInterval(timer);
+ }, [targetDate]);
+
+ if (
+ timeLeft.days === 0 &&
+ timeLeft.hours === 0 &&
+ timeLeft.minutes === 0 &&
+ timeLeft.seconds === 0
+ ) {
+ return null;
+ }
+
+ return (
+
+ {["days", "hours", "minutes", "seconds"].map((unit, index) => (
+
+ {index > 0 && : }
+
+
+ {formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(0)}
+
+
+ {formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(1)}
+
+
{unit}
+
+
+ ))}
+
+ );
+};
+
+export default Countdown;
diff --git a/usehooks.com/src/components/HookDescription.astro b/usehooks.com/src/components/HookDescription.astro
index 0206b02..3ca4182 100644
--- a/usehooks.com/src/components/HookDescription.astro
+++ b/usehooks.com/src/components/HookDescription.astro
@@ -16,11 +16,15 @@ const { name } = Astro.props;
class="logo"
alt="React.gg"
/>
- Want to learn how to build {name} yourself? Check out react.gg – the interactive way to master modern React.
+
+ Want to learn how to build {name} yourself? Check out react.gg – the interactive way to master modern React.
+
const copyButton = document.querySelector(`.copy-btn`);
-
- const copyText = `npm i @uidotdev/usehooks`;
+ const copyText = document.querySelector(`.install code`).textContent;
copyButton.addEventListener("click", async () => {
try {
@@ -45,7 +44,7 @@ const { class: className } = Astro.props;
display: flex;
justify-content: space-between;
align-items: center;
- gap: clamp(.7rem, 3vw, 2rem);
+ gap: clamp(0.7rem, 3vw, 2rem);
background-color: var(--charcoal);
padding: clamp(1rem, 4vw, 1.3rem) clamp(1rem, 4vw, 1.5rem);
border-radius: 0.5rem;
@@ -67,7 +66,7 @@ const { class: className } = Astro.props;
.copy-btn {
padding: 0.2rem 0.4rem;
padding-left: 1.7rem;
- background: no-repeat url('/img/icon-copy.svg') 0.4rem 50% / .9rem auto;
+ background: no-repeat url("/img/icon-copy.svg") 0.4rem 50% / 0.9rem auto;
border: var(--border-light);
border-radius: 0.3rem;
font-size: clamp(0.7rem, 2vw, 0.9rem);
diff --git a/usehooks.com/src/components/QueryGGBanner.astro b/usehooks.com/src/components/QueryGGBanner.astro
new file mode 100644
index 0000000..451e6c2
--- /dev/null
+++ b/usehooks.com/src/components/QueryGGBanner.astro
@@ -0,0 +1,138 @@
+---
+import Button from "./Button.astro";
+import CountdownTimer from "./CountdownTimer";
+---
+
+
+
+
+
+
+
+
react.gg Launch Sale
+
Get 30% off through May 23rd
+
+
+
+
+
+
+
+
diff --git a/usehooks.com/src/components/search/Callout.tsx b/usehooks.com/src/components/search/Callout.tsx
index 34756bb..6b85077 100644
--- a/usehooks.com/src/components/search/Callout.tsx
+++ b/usehooks.com/src/components/search/Callout.tsx
@@ -1,19 +1,25 @@
-import styles from './Callout.module.css';
+import styles from "./Callout.module.css";
type Props = {
- image: string
- imageWidth: string
- imageHeight: string
- imageAlt: string
- pitch: string
-}
+ image: string;
+ imageWidth: string;
+ imageHeight: string;
+ imageAlt: string;
+ pitch: string;
+};
-export default function Callout({ image, imageWidth, imageHeight, imageAlt, pitch }) {
+export default function Callout({
+ image,
+ imageWidth,
+ imageHeight,
+ imageAlt,
+ pitch,
+}) {
return (
-
+
{pitch}
- )
-}
\ No newline at end of file
+ );
+}
diff --git a/usehooks.com/src/content/hooks/useContinuousRetry.mdx b/usehooks.com/src/content/hooks/useContinuousRetry.mdx
index e2549c2..9d088d6 100644
--- a/usehooks.com/src/content/hooks/useContinuousRetry.mdx
+++ b/usehooks.com/src/content/hooks/useContinuousRetry.mdx
@@ -27,10 +27,11 @@ import StaticCodeContainer from "../../components/StaticCodeContainer.astro";
### Parameters
- | Name | Type | Description |
- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
- | callback | function | The callback function to be executed repeatedly until it returns a truthy value. |
- | interval | number | (Optional) The interval in milliseconds at which the callback function is executed. Default value is 100 milliseconds. |
+ | Name | Type | Description |
+ | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------|
+ | callback | function | The callback function to be executed repeatedly until it returns a truthy value. |
+ | interval | number | (Optional) The interval in milliseconds at which the callback function is executed. Default value is 100 milliseconds. |
+ | options | object | (Optional) An object containing a `maxRetries` property which tells `useContinuousRetry` the maximum amount of retry attempts it should make before stopping |
### Return Value
diff --git a/usehooks.com/src/content/hooks/useCopyToClipboard.mdx b/usehooks.com/src/content/hooks/useCopyToClipboard.mdx
index b42ad4b..555fb0c 100644
--- a/usehooks.com/src/content/hooks/useCopyToClipboard.mdx
+++ b/usehooks.com/src/content/hooks/useCopyToClipboard.mdx
@@ -23,14 +23,6 @@ import StaticCodeContainer from "../../components/StaticCodeContainer.astro";
- ### Parameters
-
-
- | Name | Type | Description |
- | ----- | ------ | ---------------------------------------- |
- | value | string | The value to be copied to the clipboard. |
-
-
### Return Value
The `useCopyToClipboard` hook returns an array with the following elements:
diff --git a/usehooks.com/src/content/hooks/useDefault.mdx b/usehooks.com/src/content/hooks/useDefault.mdx
index 6c4bad8..655ea3b 100644
--- a/usehooks.com/src/content/hooks/useDefault.mdx
+++ b/usehooks.com/src/content/hooks/useDefault.mdx
@@ -13,13 +13,7 @@ import HookDescription from "../../components/HookDescription.astro";
import StaticCodeContainer from "../../components/StaticCodeContainer.astro";
- The useDefault hook is useful for managing state in functional components with
- default values. The hook then checks if the "state" is undefined or null. If
- it is, it returns an array containing the "defaultValue" and the "setState"
- function, allowing the component to set a default value and update the state
- accordingly. On the other hand, if the "state" is defined, it returns an array
- with the current "state" value and the "setState" function, enabling normal
- state management.
+ The `useDefault` hook behaves similar to `useState` but with one difference – if the state of the hook is `undefined` or `null`, `useDefault` will default the state to a provided default value.
@@ -28,7 +22,7 @@ import StaticCodeContainer from "../../components/StaticCodeContainer.astro";
| Name | Type | Description |
| ------------ | -------- | ----------- |
- | initialValue | any | This is the initial state value provided when calling `useDefault`. |
+ | initialValue | any | The initial value of the state returned from `useDefault` |
| defaultValue | any | The default value to be used if the state is `undefined` or `null`. |
diff --git a/usehooks.com/src/content/hooks/useGeolocation.mdx b/usehooks.com/src/content/hooks/useGeolocation.mdx
index bf58e84..16c42ba 100644
--- a/usehooks.com/src/content/hooks/useGeolocation.mdx
+++ b/usehooks.com/src/content/hooks/useGeolocation.mdx
@@ -24,15 +24,10 @@ import StaticCodeContainer from "../../components/StaticCodeContainer.astro";
### Parameters
-{" "}
-
- | Name | Type | Description | | ------- | ------ | ----------- | | options |
- object | This is an optional configuration object provided when calling
- `useGeolocation`. It is used when calling
- `navigator.geolocation.getCurrentPosition()` and
- `navigator.geolocation.watchPosition()`. Some of the attributes it accepts are
- `enableHighAccuracy`, `timeout`, and `maximumAge`. |
+| Name | Type | Description |
+| ------- | ------ | ----------- |
+| options | object | This is an optional configuration object provided when calling `useGeolocation`. It is used when calling `navigator.geolocation.getCurrentPosition()` and `navigator.geolocation.watchPosition()`. Some of the attributes it accepts are `enableHighAccuracy`, `timeout`, and `maximumAge`. |
### Return Values
diff --git a/usehooks.com/src/content/hooks/useIsClient.mdx b/usehooks.com/src/content/hooks/useIsClient.mdx
index 8492ef3..9efdd84 100644
--- a/usehooks.com/src/content/hooks/useIsClient.mdx
+++ b/usehooks.com/src/content/hooks/useIsClient.mdx
@@ -13,11 +13,7 @@ import HookDescription from "../../components/HookDescription.astro";
import StaticCodeContainer from "../../components/StaticCodeContainer.astro";
- The useIsClient hook is useful for determining whether the code is running on
- the client-side or server-side. The hook initializes a state variable called
- "isClient" and sets its initial value to false. This information can be
- utilized in conditional rendering, handling browser-specific behavior, or
- making API calls that should only be executed on the client-side.
+ useIsClient is useful for determining if it's safe to run certain client-only hooks like useMediaQuery or useLocalStorage. It returns a boolean determining if React's useEffect hook has finished running (which means the app is being rendered on the client and it's safe to use browser specific APIs).
diff --git a/usehooks.com/src/content/hooks/useLocalStorage.mdx b/usehooks.com/src/content/hooks/useLocalStorage.mdx
index f1b46f1..cdff602 100644
--- a/usehooks.com/src/content/hooks/useLocalStorage.mdx
+++ b/usehooks.com/src/content/hooks/useLocalStorage.mdx
@@ -1,6 +1,5 @@
---
name: useLocalStorage
-experimental: true
rank: 2
tagline: Store, retrieve, and synchronize data from the browser’s localStorage API with useLocalStorage
sandboxId: uselocalstorage-e6v6ey
diff --git a/usehooks.com/src/content/hooks/useScript.mdx b/usehooks.com/src/content/hooks/useScript.mdx
index d11fa79..7a6d81a 100644
--- a/usehooks.com/src/content/hooks/useScript.mdx
+++ b/usehooks.com/src/content/hooks/useScript.mdx
@@ -17,8 +17,7 @@ import StaticCodeContainer from "../../components/StaticCodeContainer.astro";
scripts into a React component. It manages the loading and status of the
script, allowing you to conditionally render components or perform actions
based on whether the script has been successfully loaded or encountered an
- error. The hook keeps track of the script’s status, such as "loading,"
- "ready," or "error," and provides this status as a return value. Additionally,
+ error. The hook keeps track of the script’s status and provides this status as a return value. Additionally,
it offers options to remove the script when the component is unmounted,
ensuring proper cleanup.
@@ -38,7 +37,7 @@ import StaticCodeContainer from "../../components/StaticCodeContainer.astro";
| Name | Type | Description |
| ------ | ------ | ----------- |
- | status | string | This represents the status of the script load. Possible values are: `idle`, `loading`, `ready`, and `error`. |
+ | status | string | This represents the status of the script load, `loading`, `ready`, `error`, or `unknown`. An `unknown` script is one that previously exists in the document, but was not added via `useScript`. |
diff --git a/usehooks.com/src/content/hooks/useSessionStorage.mdx b/usehooks.com/src/content/hooks/useSessionStorage.mdx
index 10208c1..0405fa9 100644
--- a/usehooks.com/src/content/hooks/useSessionStorage.mdx
+++ b/usehooks.com/src/content/hooks/useSessionStorage.mdx
@@ -1,6 +1,5 @@
---
name: useSessionStorage
-experimental: true
rank: 9
tagline: Store, retrieve, and synchronize data from the browser’s session storage with useSessionStorage.
sandboxId: usesessionstorage-yoodu4
diff --git a/usehooks.com/src/layouts/Layout.astro b/usehooks.com/src/layouts/Layout.astro
index eab3712..ba8d9f6 100644
--- a/usehooks.com/src/layouts/Layout.astro
+++ b/usehooks.com/src/layouts/Layout.astro
@@ -13,12 +13,12 @@ const {
ogImage = new URL("/meta/og.jpg", Astro.url),
} = Astro.props;
-const pathname = Astro.url.pathname
-
-const url = pathname[pathname.length - 1] === "/"
- ? new URL(pathname.slice(0, pathname.length -1), Astro.site)
- : new URL(Astro.url.pathname, Astro.site)
+const pathname = Astro.url.pathname;
+const url =
+ pathname[pathname.length - 1] === "/"
+ ? new URL(pathname.slice(0, pathname.length - 1), Astro.site)
+ : new URL(Astro.url.pathname, Astro.site);
---
@@ -90,6 +90,34 @@ const url = pathname[pathname.length - 1] === "/"
data-api="/stats/api/event"
data-domain="usehooks.com"
>
+
+
diff --git a/usehooks.com/src/sections/Footer.astro b/usehooks.com/src/sections/Footer.astro
index c6ce3dc..cde897a 100644
--- a/usehooks.com/src/sections/Footer.astro
+++ b/usehooks.com/src/sections/Footer.astro
@@ -2,57 +2,59 @@
---
diff --git a/usehooks.com/src/styles/globals.css b/usehooks.com/src/styles/globals.css
index 8ee6d8a..5a78f2a 100644
--- a/usehooks.com/src/styles/globals.css
+++ b/usehooks.com/src/styles/globals.css
@@ -152,7 +152,7 @@ video {
--brand-pink: #f38ba3;
--brand-green: #0ba95b;
--brand-purple: #7b5ea7;
- --brand-biege: #f9f4da;
+ --brand-beige: #f9f4da;
--brand-blue: #12b5e5;
--brand-orange: #fc7428;
--brand-red: #ed203d;
@@ -161,7 +161,7 @@ video {
--magesticPurple: #9d7dce;
--red: var(--brand-red);
- --white: var(--brand-biege);
+ --white: var(--brand-beige);
--purple: var(--brand-purple);
--black: var(--brand-coal);
--blue: var(--brand-blue);
@@ -248,7 +248,6 @@ h2 {
font-size: var(--font-md);
}
-
small {
font-size: var(--font-sm);
}