diff --git a/README.md b/README.md index 07a3a7c..44401d9 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,19 @@ Reusable javascript utilities that serve [Matt Crowder!](https://twitter.com/mcrowder65) +### Usage in tests + +Under the use-local-storage-set-state module is obviously using localStorage. +When using this with jest, you will run into errors since jest does not have localStorage. +Which is why this package requires `jest-localstorage-mock` as a peerDependency. + +In order to get your tests to work, run `npm install -D jest-localstorage-mock`, and then in your jest configuration, add: + +```json +"setupFiles": [ + "jest-localstorage-mock" +] +``` #### How to publish to npm While on the master branch, run `npm version $versionType` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d4b3e51..d884264 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5243,6 +5243,12 @@ "pretty-format": "^24.5.0" } }, + "jest-localstorage-mock": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jest-localstorage-mock/-/jest-localstorage-mock-2.4.0.tgz", + "integrity": "sha512-/mC1JxnMeuIlAaQBsDMilskC/x/BicsQ/BXQxEOw+5b1aGZkkOAqAF3nu8yq449CpzGtp5jJ5wCmDNxLgA2m6A==", + "dev": true + }, "jest-matcher-utils": { "version": "24.5.0", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-24.5.0.tgz", @@ -7649,6 +7655,11 @@ "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", "dev": true }, + "store": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/store/-/store-2.0.12.tgz", + "integrity": "sha1-jFNOKguDH3K3X8XxEZhXxE711ZM=" + }, "stream-browserify": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", diff --git a/package.json b/package.json index a204015..1c5560a 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,9 @@ "src/**/*.js", "!src/index.js" ], + "setupFiles": [ + "jest-localstorage-mock" + ], "setupFilesAfterEnv": [ "/test/config.js" ], @@ -42,6 +45,7 @@ } }, "peerDependencies": { + "jest-localstorage-mock": "2.4.0", "@material-ui/core": "3.9.2", "react": "16.8.4", "react-dom": "16.8.4" @@ -59,9 +63,13 @@ "eslint-config-mcrowder65": "0.0.46", "jest": "24.4.0", "jest-dom": "3.0.1", + "jest-localstorage-mock": "2.4.0", "react": "16.8.4", "react-dom": "16.8.4", "react-testing-library": "6.0.0" }, - "babelConfig": ".babelrc.js" + "babelConfig": ".babelrc.js", + "dependencies": { + "store": "2.0.12" + } } diff --git a/src/__tests__/useLocalStorageSetState.test.js b/src/__tests__/useLocalStorageSetState.test.js new file mode 100644 index 0000000..b7270c0 --- /dev/null +++ b/src/__tests__/useLocalStorageSetState.test.js @@ -0,0 +1,121 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { render, fireEvent } from "react-testing-library"; + +import useLocalStorageSetState from "../useLocalStorageSetState"; + +function MyComponent() { + const [name, setName] = useLocalStorageSetState("", "name"); + const [flag, setFlag] = useLocalStorageSetState(false, "flag"); + const [array, setArray] = useLocalStorageSetState([], "array"); + return ( +
+ {name} + setName(e.target.value)} + /> + + + {array.join(",")} +
+ ); +} + +test("that after inserting text into the document then remounting, the text will remain", () => { + const { queryByText, queryByTestId, unmount, rerender } = render( + , + ); + + const newValue = "Lebron James"; + fireEvent.change(queryByTestId("name"), { target: { value: newValue } }); + + expect(queryByText(newValue)).toBeInTheDocument(); + + unmount(); + + rerender(); + + expect(queryByText(newValue)).toBeInTheDocument(); +}); + +test("that on component reloads, booleans persist", () => { + const { getByText, getByTestId, unmount, rerender } = render(); + + fireEvent.click(getByTestId("button")); + + expect(getByText(/true/i)).toBeInTheDocument(); + + unmount(); + + rerender(); + + expect(getByText(/true/i)).toBeInTheDocument(); +}); + +test("that on component reloads, arrays persist", () => { + const { getByText, getByTestId, unmount, rerender } = render(); + + fireEvent.click(getByTestId("array-button")); + + expect(getByText(/1/i)).toBeInTheDocument(); + + fireEvent.click(getByTestId("array-button")); + + expect(getByText(/1,2/i)).toBeInTheDocument(); + + unmount(); + + rerender(); + + expect(getByText(/1,2/i)).toBeInTheDocument(); +}); + +test("that it throws when no name is provided", () => { + expect(() => useLocalStorageSetState("asdf")).toThrow(); +}); + +test("that when passing a function, it utilizes it to set the value", () => { + function MyComp(props) { + const [index, setIndex] = useLocalStorageSetState((prev) => { + if (prev >= props.arr.length || prev === undefined) { + return 0; + } else { + return prev; + } + }, "index-local-storage"); + return ( +
+
+ ); + } + + MyComp.propTypes = { + arr: PropTypes.array.isRequired, + }; + + const { getByTestId, getByText, rerender, unmount } = render( + , + ); + + expect(getByText(/first/i)).toBeInTheDocument(); + + fireEvent.click(getByTestId("set-index-1")); + + expect(getByText(/second/i)).toBeInTheDocument(); + + unmount(); + rerender(); + + expect(getByText(/first/i)).toBeInTheDocument(); +}); diff --git a/src/index.js b/src/index.js index 8b2e4d9..a894b67 100644 --- a/src/index.js +++ b/src/index.js @@ -9,6 +9,7 @@ import LoaderButton from "./LoaderButton"; import LoaderCard from "./LoaderCard"; import Login from "./Login"; import Signup from "./Signup"; +import useLocalStorageSetState from "./useLocalStorageSetState"; export { useArrowKeyListener, @@ -22,4 +23,5 @@ export { LoaderCard, Login, Signup, + useLocalStorageSetState, }; diff --git a/src/useLocalStorageSetState.js b/src/useLocalStorageSetState.js new file mode 100644 index 0000000..4e1d95a --- /dev/null +++ b/src/useLocalStorageSetState.js @@ -0,0 +1,30 @@ +import React from "react"; +import store from "store"; +function useLocalStorageSetState(initialValue, name) { + if (!name) { + throw new Error( + "Name must be provided to persist properly to localStorage", + ); + } + let actualInitialValue = + store.get(name) !== undefined ? store.get(name) : initialValue; + if (typeof initialValue === "function") { + actualInitialValue = initialValue(actualInitialValue); + } + const [value, setValue] = React.useState(actualInitialValue); + + const theirSetValue = (theirNewValue) => { + let valueToSet; + if (typeof theirNewValue === "function") { + valueToSet = theirNewValue(value); + setValue(valueToSet); + } else { + setValue(theirNewValue); + valueToSet = theirNewValue; + } + store.set(name, valueToSet); + }; + return [value, theirSetValue]; +} + +export default useLocalStorageSetState;