Fix initial list settings (#1401)
* Allow ocalStorage values to be initialized by cb * Initialize list settings if not found * Fix settings merging * Refactor types
This commit is contained in:
parent
4fcc3eec5e
commit
51e204076f
9 changed files with 314 additions and 112 deletions
128
package-lock.json
generated
128
package-lock.json
generated
|
@ -6931,33 +6931,32 @@
|
|||
"integrity": "sha512-HrCIVMLjE1MOozVoD86622S7aunluLb2PJdPfb3nYiEtohm8mIB/vyv0Fd37AdeMFrTUQXEunw78YloMA3Qilg=="
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.21.0.tgz",
|
||||
"integrity": "sha512-FPUyCPKZbVGexmbCFI3EQHzCZdy2/5f+jv6k2EDljGdXSRc0cKvbndd2nHZkSLqCNOPk0jB6lGzwIkglXcYVsQ==",
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.31.0.tgz",
|
||||
"integrity": "sha512-iPKZTZNavAlOhfF4gymiSuUkgLne/nh5Oz2/mdiUmuZVD42m9PapnCnzjxuDsnpnbH3wT5s2D8bw6S39TC6GNw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/experimental-utils": "4.21.0",
|
||||
"@typescript-eslint/scope-manager": "4.21.0",
|
||||
"debug": "^4.1.1",
|
||||
"@typescript-eslint/experimental-utils": "4.31.0",
|
||||
"@typescript-eslint/scope-manager": "4.31.0",
|
||||
"debug": "^4.3.1",
|
||||
"functional-red-black-tree": "^1.0.1",
|
||||
"lodash": "^4.17.15",
|
||||
"regexpp": "^3.0.0",
|
||||
"semver": "^7.3.2",
|
||||
"tsutils": "^3.17.1"
|
||||
"regexpp": "^3.1.0",
|
||||
"semver": "^7.3.5",
|
||||
"tsutils": "^3.21.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/experimental-utils": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.21.0.tgz",
|
||||
"integrity": "sha512-cEbgosW/tUFvKmkg3cU7LBoZhvUs+ZPVM9alb25XvR0dal4qHL3SiUqHNrzoWSxaXA9gsifrYrS1xdDV6w/gIA==",
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.31.0.tgz",
|
||||
"integrity": "sha512-Hld+EQiKLMppgKKkdUsLeVIeEOrwKc2G983NmznY/r5/ZtZCDvIOXnXtwqJIgYz/ymsy7n7RGvMyrzf1WaSQrw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.3",
|
||||
"@typescript-eslint/scope-manager": "4.21.0",
|
||||
"@typescript-eslint/types": "4.21.0",
|
||||
"@typescript-eslint/typescript-estree": "4.21.0",
|
||||
"eslint-scope": "^5.0.0",
|
||||
"eslint-utils": "^2.0.0"
|
||||
"@types/json-schema": "^7.0.7",
|
||||
"@typescript-eslint/scope-manager": "4.31.0",
|
||||
"@typescript-eslint/types": "4.31.0",
|
||||
"@typescript-eslint/typescript-estree": "4.31.0",
|
||||
"eslint-scope": "^5.1.1",
|
||||
"eslint-utils": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint-scope": {
|
||||
|
@ -6973,55 +6972,55 @@
|
|||
}
|
||||
},
|
||||
"@typescript-eslint/parser": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.21.0.tgz",
|
||||
"integrity": "sha512-eyNf7QmE5O/l1smaQgN0Lj2M/1jOuNg2NrBm1dqqQN0sVngTLyw8tdCbih96ixlhbF1oINoN8fDCyEH9SjLeIA==",
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.31.0.tgz",
|
||||
"integrity": "sha512-oWbzvPh5amMuTmKaf1wp0ySxPt2ZXHnFQBN2Szu1O//7LmOvgaKTCIDNLK2NvzpmVd5A2M/1j/rujBqO37hj3w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/scope-manager": "4.21.0",
|
||||
"@typescript-eslint/types": "4.21.0",
|
||||
"@typescript-eslint/typescript-estree": "4.21.0",
|
||||
"debug": "^4.1.1"
|
||||
"@typescript-eslint/scope-manager": "4.31.0",
|
||||
"@typescript-eslint/types": "4.31.0",
|
||||
"@typescript-eslint/typescript-estree": "4.31.0",
|
||||
"debug": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/scope-manager": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.21.0.tgz",
|
||||
"integrity": "sha512-kfOjF0w1Ix7+a5T1knOw00f7uAP9Gx44+OEsNQi0PvvTPLYeXJlsCJ4tYnDj5PQEYfpcgOH5yBlw7K+UEI9Agw==",
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.31.0.tgz",
|
||||
"integrity": "sha512-LJ+xtl34W76JMRLjbaQorhR0hfRAlp3Lscdiz9NeI/8i+q0hdBZ7BsiYieLoYWqy+AnRigaD3hUwPFugSzdocg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.21.0",
|
||||
"@typescript-eslint/visitor-keys": "4.21.0"
|
||||
"@typescript-eslint/types": "4.31.0",
|
||||
"@typescript-eslint/visitor-keys": "4.31.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/types": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.21.0.tgz",
|
||||
"integrity": "sha512-+OQaupjGVVc8iXbt6M1oZMwyKQNehAfLYJJ3SdvnofK2qcjfor9pEM62rVjBknhowTkh+2HF+/KdRAc/wGBN2w==",
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.31.0.tgz",
|
||||
"integrity": "sha512-9XR5q9mk7DCXgXLS7REIVs+BaAswfdHhx91XqlJklmqWpTALGjygWVIb/UnLh4NWhfwhR5wNe1yTyCInxVhLqQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.21.0.tgz",
|
||||
"integrity": "sha512-ZD3M7yLaVGVYLw4nkkoGKumb7Rog7QID9YOWobFDMQKNl+vPxqVIW/uDk+MDeGc+OHcoG2nJ2HphwiPNajKw3w==",
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.31.0.tgz",
|
||||
"integrity": "sha512-QHl2014t3ptg+xpmOSSPn5hm4mY8D4s97ftzyk9BZ8RxYQ3j73XcwuijnJ9cMa6DO4aLXeo8XS3z1omT9LA/Eg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.21.0",
|
||||
"@typescript-eslint/visitor-keys": "4.21.0",
|
||||
"debug": "^4.1.1",
|
||||
"globby": "^11.0.1",
|
||||
"@typescript-eslint/types": "4.31.0",
|
||||
"@typescript-eslint/visitor-keys": "4.31.0",
|
||||
"debug": "^4.3.1",
|
||||
"globby": "^11.0.3",
|
||||
"is-glob": "^4.0.1",
|
||||
"semver": "^7.3.2",
|
||||
"tsutils": "^3.17.1"
|
||||
"semver": "^7.3.5",
|
||||
"tsutils": "^3.21.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/visitor-keys": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.21.0.tgz",
|
||||
"integrity": "sha512-dH22dROWGi5Z6p+Igc8bLVLmwy7vEe8r+8c+raPQU0LxgogPUrRAtRGtvBWmlr9waTu3n+QLt/qrS/hWzk1x5w==",
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.31.0.tgz",
|
||||
"integrity": "sha512-HUcRp2a9I+P21+O21yu3ezv3GEPGjyGiXoEUQwZXjR8UxRApGeLyWH4ZIIUSalE28aG4YsV6GjtaAVB3QKOu0w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.21.0",
|
||||
"@typescript-eslint/types": "4.31.0",
|
||||
"eslint-visitor-keys": "^2.0.0"
|
||||
}
|
||||
},
|
||||
|
@ -14117,26 +14116,18 @@
|
|||
}
|
||||
},
|
||||
"eslint-utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
|
||||
"integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
|
||||
"integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"eslint-visitor-keys": "^1.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint-visitor-keys": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
|
||||
"integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
|
||||
"dev": true
|
||||
}
|
||||
"eslint-visitor-keys": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"eslint-visitor-keys": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz",
|
||||
"integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
|
||||
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
|
||||
"dev": true
|
||||
},
|
||||
"espree": {
|
||||
|
@ -25120,9 +25111,9 @@
|
|||
}
|
||||
},
|
||||
"regexpp": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz",
|
||||
"integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
|
||||
"integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
|
||||
"dev": true
|
||||
},
|
||||
"regexpu-core": {
|
||||
|
@ -29125,9 +29116,10 @@
|
|||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz",
|
||||
"integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg=="
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
|
||||
"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
|
||||
"dev": true
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "0.7.28",
|
||||
|
|
|
@ -77,7 +77,6 @@
|
|||
"react-sortable-tree": "^2.6.2",
|
||||
"semver-compare": "^1.0.0",
|
||||
"slugify": "^1.4.6",
|
||||
"typescript": "^4.2.3",
|
||||
"url-join": "^4.0.1",
|
||||
"use-react-router": "^1.0.7"
|
||||
},
|
||||
|
@ -178,6 +177,7 @@
|
|||
"start-server-and-test": "^1.11.0",
|
||||
"ts-jest": "^24.2.0",
|
||||
"tsconfig-paths-webpack-plugin": "^3.2.0",
|
||||
"typescript": "^4.3.5",
|
||||
"webpack": "^4.35.3",
|
||||
"webpack-bundle-analyzer": "^4.4.1",
|
||||
"webpack-cli": "^3.3.6",
|
||||
|
@ -194,6 +194,7 @@
|
|||
"fsevents": "^1.2.9"
|
||||
},
|
||||
"jest": {
|
||||
"resetMocks": false,
|
||||
"setupFiles": [
|
||||
"jest-localstorage-mock"
|
||||
],
|
||||
|
|
|
@ -53,10 +53,9 @@ interface AppsListProps {
|
|||
|
||||
export const AppsList: React.FC<AppsListProps> = ({ params }) => {
|
||||
const { action } = params;
|
||||
const [activeInstallations, setActiveInstallations] = useLocalStorage(
|
||||
"activeInstallations",
|
||||
[]
|
||||
);
|
||||
const [activeInstallations, setActiveInstallations] = useLocalStorage<
|
||||
Array<Record<"id" | "name", string>>
|
||||
>("activeInstallations", []);
|
||||
const notify = useNotifier();
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigator();
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { DEMO_MODE } from "@saleor/config";
|
||||
import { User } from "@saleor/fragments/types/User";
|
||||
import { SetLocalStorage } from "@saleor/hooks/useLocalStorage";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { getFullName, getMutationStatus } from "@saleor/misc";
|
||||
import errorTracker from "@saleor/services/errorTracking";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
|
||||
import { useMutation } from "react-apollo";
|
||||
|
||||
import {
|
||||
|
@ -57,7 +56,7 @@ export interface UseExternalAuthProvider extends UseAuthProvider {
|
|||
) => Promise<ExternalObtainAccessTokens_externalObtainAccessTokens>;
|
||||
}
|
||||
export interface UseExternalAuthProviderOpts extends UseAuthProviderOpts {
|
||||
setAuthPlugin: SetLocalStorage<any>;
|
||||
setAuthPlugin: Dispatch<SetStateAction<any>>;
|
||||
authPlugin: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { DEMO_MODE } from "@saleor/config";
|
||||
import { User } from "@saleor/fragments/types/User";
|
||||
import { SetLocalStorage } from "@saleor/hooks/useLocalStorage";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { getFullName, getMutationStatus } from "@saleor/misc";
|
||||
import errorTracker from "@saleor/services/errorTracking";
|
||||
|
@ -9,7 +8,7 @@ import {
|
|||
login as loginWithCredentialsManagementAPI,
|
||||
saveCredentials
|
||||
} from "@saleor/utils/credentialsManagement";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
|
||||
import { useMutation } from "react-apollo";
|
||||
|
||||
import {
|
||||
|
@ -38,7 +37,7 @@ export interface UseSaleorAuthProvider extends UseAuthProvider {
|
|||
loginByToken: (auth: string, csrf: string, user: User) => void;
|
||||
}
|
||||
export interface UseSaleorAuthProviderOpts extends UseAuthProviderOpts {
|
||||
setAuthPlugin: SetLocalStorage<any>;
|
||||
setAuthPlugin: Dispatch<SetStateAction<any>>;
|
||||
authPlugin: string;
|
||||
}
|
||||
|
||||
|
|
80
src/hooks/useListSettings.test.ts
Normal file
80
src/hooks/useListSettings.test.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import { defaultListSettings } from "@saleor/config";
|
||||
import { ListViews } from "@saleor/types";
|
||||
import { renderHook } from "@testing-library/react-hooks";
|
||||
|
||||
import useListSettings, { listSettingsStorageKey } from "./useListSettings";
|
||||
|
||||
const key = ListViews.CATEGORY_LIST;
|
||||
const storedValue = {
|
||||
...defaultListSettings,
|
||||
[key]: {
|
||||
...defaultListSettings[key],
|
||||
rowNumber: 100
|
||||
}
|
||||
};
|
||||
const valueWithoutKey = {
|
||||
...defaultListSettings,
|
||||
[key]: undefined
|
||||
};
|
||||
const valueWithoutSettings = {
|
||||
...defaultListSettings,
|
||||
[key]: {
|
||||
foo: "bar"
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
describe("useListSettings", () => {
|
||||
it("properly inits from value", () => {
|
||||
expect(localStorage.getItem(listSettingsStorageKey)).toBe(null);
|
||||
|
||||
const { result } = renderHook(() => useListSettings(key));
|
||||
|
||||
expect(result.current.settings).toStrictEqual(defaultListSettings[key]);
|
||||
});
|
||||
|
||||
it("omits init if value is present", () => {
|
||||
localStorage.setItem(listSettingsStorageKey, JSON.stringify(storedValue));
|
||||
expect(localStorage.getItem(listSettingsStorageKey)).toBe(
|
||||
JSON.stringify(storedValue)
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useListSettings(key));
|
||||
|
||||
expect(result.current.settings).toStrictEqual(storedValue[key]);
|
||||
});
|
||||
|
||||
it("properly merges new default values to saved ones", () => {
|
||||
localStorage.setItem(
|
||||
listSettingsStorageKey,
|
||||
JSON.stringify(valueWithoutKey)
|
||||
);
|
||||
expect(localStorage.getItem(listSettingsStorageKey)).toBe(
|
||||
JSON.stringify(valueWithoutKey)
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useListSettings(key));
|
||||
|
||||
expect(result.current.settings).toStrictEqual(defaultListSettings[key]);
|
||||
});
|
||||
|
||||
it("properly fills missing settings", () => {
|
||||
localStorage.setItem(
|
||||
listSettingsStorageKey,
|
||||
JSON.stringify(valueWithoutSettings)
|
||||
);
|
||||
expect(localStorage.getItem(listSettingsStorageKey)).toBe(
|
||||
JSON.stringify(valueWithoutSettings)
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useListSettings(key));
|
||||
|
||||
expect(result.current.settings).toStrictEqual({
|
||||
...valueWithoutSettings[key],
|
||||
...defaultListSettings[key]
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,9 +1,10 @@
|
|||
import useLocalStorage from "@saleor/hooks/useLocalStorage";
|
||||
import { useEffect } from "react";
|
||||
import merge from "lodash/merge";
|
||||
|
||||
import { AppListViewSettings, defaultListSettings } from "./../config";
|
||||
import { ListSettings, ListViews } from "./../types";
|
||||
|
||||
export const listSettingsStorageKey = "listConfig";
|
||||
export interface UseListSettings<TColumns extends string = string> {
|
||||
settings: ListSettings<TColumns>;
|
||||
updateListSettings: <T extends keyof ListSettings<TColumns>>(
|
||||
|
@ -15,18 +16,15 @@ export default function useListSettings<TColumns extends string = string>(
|
|||
listName: ListViews
|
||||
): UseListSettings<TColumns> {
|
||||
const [settings, setListSettings] = useLocalStorage<AppListViewSettings>(
|
||||
"listConfig",
|
||||
defaultListSettings
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (settings[listName] === undefined) {
|
||||
setListSettings(settings => ({
|
||||
...settings,
|
||||
[listName]: defaultListSettings[listName]
|
||||
}));
|
||||
listSettingsStorageKey,
|
||||
storedListSettings => {
|
||||
if (typeof storedListSettings !== "object") {
|
||||
return defaultListSettings;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return merge({}, defaultListSettings, storedListSettings);
|
||||
}
|
||||
);
|
||||
|
||||
const updateListSettings = <T extends keyof ListSettings>(
|
||||
key: T,
|
||||
|
|
105
src/hooks/useLocalStorage.test.ts
Normal file
105
src/hooks/useLocalStorage.test.ts
Normal file
|
@ -0,0 +1,105 @@
|
|||
import { renderHook } from "@testing-library/react-hooks";
|
||||
|
||||
import useLocalStorage from "./useLocalStorage";
|
||||
|
||||
const key = "exampleKey";
|
||||
const initialValue = "exampleValue";
|
||||
const savedValue = "savedValue";
|
||||
const numberValue = 12;
|
||||
const objectValue = { foo: numberValue };
|
||||
const booleanValue = true;
|
||||
const postfix = "-test";
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
describe("useLocalStorage", () => {
|
||||
it("properly inits from value", () => {
|
||||
expect(localStorage.getItem(key)).toBe(null);
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage(key, initialValue));
|
||||
|
||||
expect(result.current[0]).toBe(initialValue);
|
||||
});
|
||||
|
||||
it("omits initializing if value is found", () => {
|
||||
localStorage.setItem(key, savedValue);
|
||||
expect(localStorage.getItem(key)).toBe(savedValue);
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage(key, initialValue));
|
||||
|
||||
expect(result.current[0]).toBe(savedValue);
|
||||
});
|
||||
|
||||
it("properly casts value to number", () => {
|
||||
localStorage.setItem(key, JSON.stringify(numberValue));
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage(key, initialValue));
|
||||
|
||||
expect(result.current[0]).toBe(numberValue);
|
||||
});
|
||||
|
||||
it("properly casts value to boolean", () => {
|
||||
localStorage.setItem(key, JSON.stringify(booleanValue));
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage(key, initialValue));
|
||||
|
||||
expect(result.current[0]).toBe(booleanValue);
|
||||
});
|
||||
|
||||
it("properly casts value to object", () => {
|
||||
localStorage.setItem(key, JSON.stringify(objectValue));
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage(key, initialValue));
|
||||
|
||||
expect(result.current[0]).toStrictEqual(objectValue);
|
||||
});
|
||||
|
||||
it("properly inits from callback if value is not found", () => {
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage(key, storedValue =>
|
||||
storedValue ? storedValue + postfix : initialValue
|
||||
)
|
||||
);
|
||||
|
||||
expect(result.current[0]).toBe(initialValue);
|
||||
});
|
||||
|
||||
it("properly inits from callback if value is found", () => {
|
||||
localStorage.setItem(key, savedValue);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage(key, storedValue =>
|
||||
storedValue ? storedValue + postfix : initialValue
|
||||
)
|
||||
);
|
||||
|
||||
expect(result.current[0]).toBe(savedValue + postfix);
|
||||
expect(localStorage.getItem(key)).toBe(savedValue + postfix);
|
||||
});
|
||||
|
||||
it("properly inits from callback if value is object", () => {
|
||||
localStorage.setItem(key, JSON.stringify(objectValue));
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage(key, storedValue => {
|
||||
if (typeof storedValue === "object") {
|
||||
return {
|
||||
...storedValue,
|
||||
bar: "baz"
|
||||
};
|
||||
}
|
||||
|
||||
return objectValue;
|
||||
})
|
||||
);
|
||||
|
||||
const newValue = {
|
||||
foo: numberValue,
|
||||
bar: "baz"
|
||||
};
|
||||
expect(result.current[0]).toStrictEqual(newValue);
|
||||
expect(localStorage.getItem(key)).toBe(JSON.stringify(newValue));
|
||||
});
|
||||
});
|
|
@ -1,33 +1,62 @@
|
|||
import { useState } from "react";
|
||||
import { Dispatch, SetStateAction, useState } from "react";
|
||||
|
||||
export type SetLocalStorageValue<T> = T | ((prevValue: T) => T);
|
||||
export type SetLocalStorage<T> = (value: SetLocalStorageValue<T>) => void;
|
||||
export type UseLocalStorage<T> = [T, Dispatch<SetStateAction<T>>];
|
||||
export default function useLocalStorage<T>(
|
||||
key: string,
|
||||
initialValue: T
|
||||
): [T, SetLocalStorage<T>] {
|
||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||
let result: T;
|
||||
initialValue: SetStateAction<T>
|
||||
): UseLocalStorage<T> {
|
||||
const saveToLocalStorage = (valueToStore: T) => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
result = item ? JSON.parse(item) : initialValue;
|
||||
} catch {
|
||||
result = initialValue;
|
||||
if (typeof valueToStore === "string") {
|
||||
localStorage.setItem(key, valueToStore);
|
||||
} else {
|
||||
localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const setValue = (value: SetLocalStorageValue<T>) => {
|
||||
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
||||
setStoredValue(valueToStore);
|
||||
|
||||
try {
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||
} catch {
|
||||
console.warn(`Could not save ${key} to localStorage`);
|
||||
}
|
||||
};
|
||||
|
||||
function getValue(value: T, initOrCb: SetStateAction<T>): T {
|
||||
if (initOrCb instanceof Function) {
|
||||
const newValue = initOrCb(value);
|
||||
saveToLocalStorage(newValue);
|
||||
return newValue;
|
||||
}
|
||||
|
||||
return value ?? initOrCb;
|
||||
}
|
||||
|
||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||
let result: T | null;
|
||||
const item = localStorage.getItem(key);
|
||||
|
||||
if (item === null) {
|
||||
return getValue(null, initialValue);
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(item);
|
||||
if (!parsed) {
|
||||
throw new Error("Empty value");
|
||||
}
|
||||
|
||||
result = parsed;
|
||||
} catch {
|
||||
// Casting to T (which should resolve to string) because JSON.parse would
|
||||
// throw an error if "foo" was passed, but properly casting "true" or "1"
|
||||
// to their respective types
|
||||
result = (item as unknown) as T;
|
||||
}
|
||||
|
||||
return getValue(result, initialValue);
|
||||
});
|
||||
|
||||
const setValue = (value: SetStateAction<T>) => {
|
||||
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
||||
setStoredValue(valueToStore);
|
||||
saveToLocalStorage(valueToStore);
|
||||
};
|
||||
|
||||
return [storedValue, setValue];
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue