Introduce useStoredState()
hook (#4351)
This commit is contained in:
parent
6cb730c613
commit
abc7306097
6 changed files with 123 additions and 35 deletions
|
@ -82,7 +82,6 @@
|
|||
"react-hook-form": "^7.42.1",
|
||||
"react-i18next": "^12.1.5",
|
||||
"react-router-dom": "6.8.0",
|
||||
"react-use-localstorage": "^3.5.3",
|
||||
"reactflow": "^11.5.1",
|
||||
"use-react-router-breadcrumbs": "^4.0.1"
|
||||
},
|
||||
|
|
|
@ -11,11 +11,11 @@ import {
|
|||
import { ExternalLinkAltIcon, HelpIcon } from "@patternfly/react-icons";
|
||||
import { PropsWithChildren, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useLocalStorage from "react-use-localstorage";
|
||||
|
||||
import helpUrls from "../../help-urls";
|
||||
import { createNamedContext } from "../../utils/createNamedContext";
|
||||
import useRequiredContext from "../../utils/useRequiredContext";
|
||||
import { useStoredState } from "../../utils/useStoredState";
|
||||
|
||||
import "./help-header.css";
|
||||
|
||||
|
@ -32,13 +32,14 @@ export const HelpContext = createNamedContext<HelpContextProps | undefined>(
|
|||
export const useHelp = () => useRequiredContext(HelpContext);
|
||||
|
||||
export const Help = ({ children }: PropsWithChildren<unknown>) => {
|
||||
const [enabled, setHelp] = useLocalStorage("helpEnabled", "true");
|
||||
const [enabled, setHelp] = useStoredState(localStorage, "helpEnabled", true);
|
||||
|
||||
function toggleHelp() {
|
||||
setHelp(enabled === "true" ? "false" : "true");
|
||||
setHelp(!enabled);
|
||||
}
|
||||
|
||||
return (
|
||||
<HelpContext.Provider value={{ enabled: enabled === "true", toggleHelp }}>
|
||||
<HelpContext.Provider value={{ enabled, toggleHelp }}>
|
||||
{children}
|
||||
</HelpContext.Provider>
|
||||
);
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
import {
|
||||
ComponentClass,
|
||||
isValidElement,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ButtonVariant } from "@patternfly/react-core";
|
||||
import type { SVGIconProps } from "@patternfly/react-icons/dist/js/createIcon";
|
||||
import {
|
||||
IAction,
|
||||
IActions,
|
||||
|
@ -20,15 +12,23 @@ import {
|
|||
TableProps,
|
||||
TableVariant,
|
||||
} from "@patternfly/react-table";
|
||||
import { get, cloneDeep, differenceBy } from "lodash-es";
|
||||
import useLocalStorage from "react-use-localstorage";
|
||||
import { cloneDeep, differenceBy, get } from "lodash-es";
|
||||
import {
|
||||
ComponentClass,
|
||||
isValidElement,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { PaginatingTableToolbar } from "./PaginatingTableToolbar";
|
||||
import { ListEmptyState } from "../list-empty-state/ListEmptyState";
|
||||
import { KeycloakSpinner } from "../keycloak-spinner/KeycloakSpinner";
|
||||
import { useFetch } from "../../context/auth/AdminClient";
|
||||
import type { SVGIconProps } from "@patternfly/react-icons/dist/js/createIcon";
|
||||
import { ButtonVariant } from "@patternfly/react-core";
|
||||
import { useStoredState } from "../../utils/useStoredState";
|
||||
import { KeycloakSpinner } from "../keycloak-spinner/KeycloakSpinner";
|
||||
import { ListEmptyState } from "../list-empty-state/ListEmptyState";
|
||||
import { PaginatingTableToolbar } from "./PaginatingTableToolbar";
|
||||
|
||||
type TitleCell = { title: JSX.Element };
|
||||
type Cell<T> = keyof T | JSX.Element | TitleCell;
|
||||
|
@ -206,11 +206,13 @@ export function KeycloakDataTable<T>({
|
|||
const [unPaginatedData, setUnPaginatedData] = useState<T[]>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [defaultPageSize, setDefaultPageSize] = useLocalStorage(
|
||||
const [defaultPageSize, setDefaultPageSize] = useStoredState(
|
||||
localStorage,
|
||||
"pageSize",
|
||||
"10"
|
||||
10
|
||||
);
|
||||
const [max, setMax] = useState(parseInt(defaultPageSize));
|
||||
|
||||
const [max, setMax] = useState(defaultPageSize);
|
||||
const [first, setFirst] = useState(0);
|
||||
const [search, setSearch] = useState<string>("");
|
||||
const prevSearch = useRef<string>();
|
||||
|
@ -410,7 +412,7 @@ export function KeycloakDataTable<T>({
|
|||
onPerPageSelect={(first, max) => {
|
||||
setFirst(first);
|
||||
setMax(max);
|
||||
setDefaultPageSize(`${max}`);
|
||||
setDefaultPageSize(max);
|
||||
}}
|
||||
inputGroupName={
|
||||
searchPlaceholderKey ? `${ariaLabelKey}input` : undefined
|
||||
|
|
57
apps/admin-ui/src/utils/useStorageItem.ts
Normal file
57
apps/admin-ui/src/utils/useStorageItem.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { Dispatch, useCallback, useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
* A hook that allows you to get a specific item stored by the [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API).
|
||||
* Automatically updates the value when modified in the context of another document (such as an open tab) trough the [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event.
|
||||
*
|
||||
* @param storageArea The storage area to target, must implement the [`Storage`](https://developer.mozilla.org/en-US/docs/Web/API/Storage) interface (such as [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) and [`sessionStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage)).
|
||||
* @param keyName The key of the item to get from storage, same as passed to [`Storage.getItem()`](https://developer.mozilla.org/en-US/docs/Web/API/Storage/getItem)
|
||||
* @param The default value to fall back to in case no stored value was retrieved.
|
||||
*/
|
||||
export function useStorageItem(
|
||||
storageArea: Storage,
|
||||
keyName: string,
|
||||
defaultValue: string
|
||||
): [string, Dispatch<string>] {
|
||||
const [value, setInnerValue] = useState(
|
||||
() => storageArea.getItem(keyName) ?? defaultValue
|
||||
);
|
||||
|
||||
const setValue = useCallback((newValue: string) => {
|
||||
setInnerValue(newValue);
|
||||
|
||||
// If the new value the same as the default value we can remove the item from storage.
|
||||
if (newValue === defaultValue) {
|
||||
storageArea.removeItem(keyName);
|
||||
} else {
|
||||
storageArea.setItem(keyName, newValue);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// If the key name, storage area or default value has changed, we want to update the value.
|
||||
// React will only set state if it actually changed, so no need to worry about re-renders.
|
||||
setInnerValue(storageArea.getItem(keyName) ?? defaultValue);
|
||||
|
||||
// Subscribe to storage events so we can update the value when it is changed within the context of another document.
|
||||
window.addEventListener("storage", handleStorage);
|
||||
|
||||
function handleStorage(event: StorageEvent) {
|
||||
// If the affected storage area is different we can ignore this event.
|
||||
// For example, if we're using session storage we're not interested in changes from local storage.
|
||||
if (event.storageArea !== storageArea) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the event key is null then it means all storage was cleared.
|
||||
// Therefore we're interested in keys that are, or that match the key name.
|
||||
if (event.key === null || event.key === keyName) {
|
||||
setValue(event.newValue ?? defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
return () => window.removeEventListener("storage", handleStorage);
|
||||
}, [storageArea, keyName, defaultValue]);
|
||||
|
||||
return [value, setValue];
|
||||
}
|
38
apps/admin-ui/src/utils/useStoredState.ts
Normal file
38
apps/admin-ui/src/utils/useStoredState.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { Dispatch, useCallback, useMemo } from "react";
|
||||
import { useStorageItem } from "./useStorageItem";
|
||||
|
||||
/**
|
||||
* A hook that acts similarly to React's `useState()`, but persists the state using [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API).
|
||||
* Automatically updates the value when modified in the context of another document (such as an open tab) trough the [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event.
|
||||
*
|
||||
* The value is serialized as [JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON) and therefore the value provided must be serializable as such.
|
||||
* Because the value is always serialized it will never be referentially equal to originally provided value.
|
||||
*
|
||||
* @param storageArea The storage area to target, must implement the [`Storage`](https://developer.mozilla.org/en-US/docs/Web/API/Storage) interface (such as [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) and [`sessionStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage)).
|
||||
* @param keyName The key of the item to get from storage, same as passed to [`Storage.getItem()`](https://developer.mozilla.org/en-US/docs/Web/API/Storage/getItem)
|
||||
* @param defaultValue The default value to fall back to in case no stored value was retrieved (must be serializable as JSON).
|
||||
*/
|
||||
export function useStoredState<S>(
|
||||
storageArea: Storage,
|
||||
keyName: string,
|
||||
defaultValue: S
|
||||
): [S, Dispatch<S>] {
|
||||
const defaultValueSerialized = useMemo(
|
||||
() => JSON.stringify(defaultValue),
|
||||
[defaultValue]
|
||||
);
|
||||
|
||||
const [storedValue, setStoredValue] = useStorageItem(
|
||||
storageArea,
|
||||
keyName,
|
||||
defaultValueSerialized
|
||||
);
|
||||
|
||||
const value = useMemo<S>(() => JSON.parse(storedValue), [storedValue]);
|
||||
const setValue = useCallback(
|
||||
(value: S) => setStoredValue(JSON.stringify(value)),
|
||||
[]
|
||||
);
|
||||
|
||||
return [value, setValue];
|
||||
}
|
9
package-lock.json
generated
9
package-lock.json
generated
|
@ -80,7 +80,6 @@
|
|||
"react-hook-form": "^7.42.1",
|
||||
"react-i18next": "^12.1.5",
|
||||
"react-router-dom": "6.8.0",
|
||||
"react-use-localstorage": "^3.5.3",
|
||||
"reactflow": "^11.5.1",
|
||||
"use-react-router-breadcrumbs": "^4.0.1"
|
||||
},
|
||||
|
@ -13922,14 +13921,6 @@
|
|||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-use-localstorage": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/react-use-localstorage/-/react-use-localstorage-3.5.3.tgz",
|
||||
"integrity": "sha512-1oNvJmo72G4v5P9ytJZZTb6ywD3UzWBiainTtfbNlb+U08hc+SOD5HqgiLTKUF0MxGcIR9JSnZGmBttNLXaQYA==",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/reactflow": {
|
||||
"version": "11.5.1",
|
||||
"resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.5.1.tgz",
|
||||
|
|
Loading…
Reference in a new issue