Introduce useStoredState() hook (#4351)

This commit is contained in:
Jon Koops 2023-02-08 13:08:46 +01:00 committed by GitHub
parent 6cb730c613
commit abc7306097
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 123 additions and 35 deletions

View file

@ -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"
},

View file

@ -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>
);

View file

@ -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

View 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];
}

View 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
View file

@ -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",