├── .gitignore ├── .yarn └── install-state.gz ├── .yarnrc.yml ├── .prettierrc ├── example.html ├── LICENSE ├── package.json ├── README.md ├── example.js ├── index.js ├── .github └── workflows │ └── codeql-analysis.yml └── .eslintrc.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | dist 4 | .cache 5 | demo -------------------------------------------------------------------------------- /.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeyparis/persistence-hooks/HEAD/.yarn/install-state.gz -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 5 | spec: "@yarnpkg/plugin-interactive-tools" 6 | 7 | yarnPath: .yarn/releases/yarn-3.1.1.cjs 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "useTabs": true, 5 | "semi": false, 6 | "singleQuote": true, 7 | "jsxSingleQuote": false, 8 | "trailingComma": "all", 9 | "bracketSpacing": true, 10 | "bracketSameLine": false, 11 | "arrowParens": "always" 12 | } 13 | -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Persistence Hooks Example 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018-present Harry Solovay (harrysolovay.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "persistence-hooks", 3 | "version": "1.2.0", 4 | "description": "React hooks that blur the line between state and persistent data", 5 | "main": "index.js", 6 | "repository": "https://github.com/joeyparis/persistence-hooks", 7 | "author": "Harry Solovay (harrysolovay.com)", 8 | "license": "MIT", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "keywords": [ 13 | "react", 14 | "use", 15 | "persistent", 16 | "state", 17 | "local", 18 | "session", 19 | "secure", 20 | "storage", 21 | "store", 22 | "react", 23 | "hook" 24 | ], 25 | "files": [ 26 | "index.*" 27 | ], 28 | "scripts": { 29 | "example": "parcel example.html" 30 | }, 31 | "peerDependencies": { 32 | "react": "^16.14.0" 33 | }, 34 | "devDependencies": { 35 | "eslint": "^8.7.0", 36 | "eslint-config-airbnb": "^19.0.4", 37 | "eslint-config-prettier": "^8.3.0", 38 | "eslint-plugin-import": "^2.25.4", 39 | "eslint-plugin-jsx-a11y": "^6.5.1", 40 | "eslint-plugin-prettier": "^4.0.0", 41 | "eslint-plugin-react": "^7.28.0", 42 | "eslint-plugin-react-hooks": "^4.3.0", 43 | "parcel": "^2.2.1", 44 | "prettier": "^2.5.1", 45 | "prop-types": "^15.8.1", 46 | "react": "^16.14.0", 47 | "react-dom": "^16.14.0" 48 | }, 49 | "ava": { 50 | "require": [ 51 | "./test-setup.js" 52 | ] 53 | }, 54 | "packageManager": "yarn@3.1.1" 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `persistence-hooks` 2 | 3 | > React hook for saving & hydrating state from local storage, session storage, or cookies 4 | 5 | ## Install 6 | 7 | ```sh 8 | yarn add persistence-hooks 9 | ``` 10 | 11 | ## Usage 12 | 13 | ### Basic Example 14 | 15 | Let's say you want a component to read from & store state in local storage: 16 | 17 | ```jsx 18 | import { useStateAndLocalStorage } from 'persistence-hooks' 19 | 20 | function MyComponent() { 21 | 22 | const INITIAL_VALUE = 'hello world' 23 | const STORAGE_KEY = 'myComponentLocalStorageKey' 24 | 25 | const [value, setValue] = useStateAndLocalStorage( 26 | INITIAL_VALUE, 27 | STORAGE_KEY, 28 | ) 29 | 30 | // use value & setValue just as you would if returned from `useState` 31 | // ... 32 | 33 | } 34 | ``` 35 | 36 | ### Available Strategies 37 | 38 | * `useStateAndLocalStorage` 39 | * `useStateAndSessionStorage` 40 | * `useStateAndCookie` 41 | 42 | ### Arguments 43 | 44 | All 3 strategies take in the following arguments: 45 | 46 | `initial`: the default value when no value has been persisted 47 | 48 | `key`: the entry in the given persisted strategy to set and draw from 49 | 50 |
51 | 52 | In `useStateAndCookie`, a 3rd argument can be passed to specify expiration. Here's the same example above, but using a 10-second cookie: 53 | 54 | ```jsx 55 | import { useStateAndCookie } from 'persistence-hooks' 56 | 57 | function MyComponent() { 58 | 59 | const INITIAL_VALUE = 'hello world' 60 | const STORAGE_KEY = 'myComponentLocalStorageKey' 61 | 62 | const [value, setValue] = useStateAndCookie( 63 | INITIAL_VALUE, 64 | STORAGE_KEY, 65 | { days: 1 / 24 / 60 / 60 * 10 }, 66 | ) 67 | 68 | // ... 69 | 70 | } 71 | ``` 72 | 73 | For more examples, check out the source. 74 | 75 | Cheers 🍻 -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { render } from 'react-dom' 4 | import { useStateAndLocalStorage, useStateAndSessionStorage, useStateAndCookie } from '.' 5 | 6 | function Counter({ title, subtitle, setCount, count }) { 7 | return ( 8 |
9 |

{title}

10 |

{subtitle}

11 | 14 | {count} 15 | 18 |
19 | ) 20 | } 21 | 22 | Counter.propTypes = { 23 | title: PropTypes.string, 24 | subtitle: PropTypes.string, 25 | setCount: PropTypes.func, 26 | count: PropTypes.number, 27 | } 28 | 29 | function LocalStorageCounter() { 30 | const [count, setCount] = useStateAndLocalStorage(0, 'localStorageKeyForCounter') 31 | return ( 32 | 37 | ) 38 | } 39 | 40 | function SessionStorageCounter() { 41 | const [count, setCount] = useStateAndSessionStorage(0, 'sessionStorageKeyForCounter') 42 | return ( 43 | 48 | ) 49 | } 50 | 51 | function CookieCounter() { 52 | const [count, setCount] = useStateAndCookie(0, 'cookieKeyForCounter', { 53 | days: (1 / 24 / 60 / 60) * 10, 54 | }) 55 | return ( 56 | 61 | ) 62 | } 63 | 64 | render( 65 |
66 |
67 | 68 | 69 | 70 |
71 |
, 72 | window.root // eslint-disable-line prettier/prettier 73 | ) 74 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { useState } = require('react') 2 | 3 | function createStorageMethods(storage, key) { 4 | return { 5 | set: (value) => { 6 | const stringified = JSON.stringify(value) 7 | storage.setItem(key, stringified) 8 | }, 9 | 10 | get: () => { 11 | const stringified = storage.getItem(key) 12 | return JSON.parse(stringified) 13 | }, 14 | } 15 | } 16 | 17 | function createLocalStorageMethods(key) { 18 | return createStorageMethods(window.localStorage, key) 19 | } 20 | 21 | function createSessionStorageMethods(key) { 22 | return createStorageMethods(window.sessionStorage, key) 23 | } 24 | 25 | function createCookieMethods(key, { days }) { 26 | return { 27 | set: (value) => { 28 | const stringified = JSON.stringify(value) 29 | let expiration = null 30 | if (days) { 31 | const currentDate = new Date() 32 | const expirationTime = currentDate.getTime() + days * 24 * 60 * 60 * 1000 33 | const expirationString = new Date(expirationTime).toUTCString() 34 | expiration = `; expires=${expirationString}` 35 | } else { 36 | expiration = '' 37 | } 38 | document.cookie = `${key}=${stringified}${expiration}; path=/` 39 | }, 40 | 41 | get: () => { 42 | const cookies = document.cookie ? document.cookie.split('; ') : [] 43 | for (let i = 0; i < cookies.length; i += 1) { 44 | const parts = cookies[i].split('=') 45 | if (parts[0] === key) { 46 | return JSON.parse(parts[1]) 47 | } 48 | } 49 | return {} 50 | }, 51 | } 52 | } 53 | 54 | function useStateAndPersistence(createMethods, initial, key, options) { 55 | const { get, set } = createMethods(key, options) 56 | 57 | const [value, setValue] = useState(() => { 58 | const persistedValue = get() 59 | return persistedValue || initial 60 | }) 61 | 62 | return [ 63 | value, 64 | (getNextValue, callback) => { 65 | const nextValue = typeof getNextValue === 'function' ? getNextValue(value) : getNextValue 66 | set(nextValue) 67 | setValue(nextValue) 68 | if (callback) callback() 69 | }, 70 | ] 71 | } 72 | 73 | function useStateAndLocalStorage(initial, key) { 74 | return useStateAndPersistence(createLocalStorageMethods, initial, key) 75 | } 76 | 77 | function useStateAndSessionStorage(initial, key) { 78 | return useStateAndPersistence(createSessionStorageMethods, initial, key) 79 | } 80 | 81 | function useStateAndCookie(initial, key, options) { 82 | return useStateAndPersistence(createCookieMethods, initial, key, options) 83 | } 84 | 85 | module.exports = { 86 | useStateAndLocalStorage, 87 | useStateAndSessionStorage, 88 | useStateAndCookie, 89 | } 90 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '43 4 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const prettierOptions = JSON.parse(fs.readFileSync(path.resolve(__dirname, '.prettierrc'), 'utf8')) 5 | 6 | module.exports = { 7 | extends: ['airbnb', 'prettier'], 8 | // 'extends': [ 9 | // 'eslint:recommended', 10 | // 'plugin:react/recommended' 11 | // ], 12 | plugins: ['prettier', 'react', 'react-hooks', 'jsx-a11y'], 13 | env: { 14 | jest: true, 15 | browser: true, 16 | node: true, 17 | es6: true, 18 | }, 19 | parserOptions: { 20 | ecmaVersion: 6, // 2018 21 | sourceType: 'module', 22 | ecmaFeatures: { 23 | // 'experimentalObjectRestSpread': true, 24 | jsx: true, 25 | }, 26 | allowImportExportEverywhere: true, 27 | }, 28 | rules: { 29 | 'prettier/prettier': ['warn', prettierOptions], // Warning because prettier and eslint sometimes conflict with each other 30 | 'arrow-body-style': [2, 'as-needed'], 31 | camelcase: 0, 32 | 'class-methods-use-this': 0, 33 | 'import/first': 0, 34 | 'import/newline-after-import': 0, 35 | 'import/no-dynamic-require': 0, 36 | 'import/no-extraneous-dependencies': 0, 37 | 'import/no-named-as-default': 0, 38 | 'import/no-unresolved': 2, 39 | 'import/no-webpack-loader-syntax': 0, 40 | 'import/prefer-default-export': 0, 41 | indent: [ 42 | 'error', 43 | 'tab', 44 | { 45 | SwitchCase: 1, 46 | }, 47 | ], 48 | 'jsx-a11y/aria-props': 2, 49 | 'jsx-a11y/heading-has-content': 0, 50 | 'jsx-a11y/href-no-hash': 'off', // Workaround for jsx-a11y/href-no-hash 51 | 'jsx-a11y/anchor-is-valid': ['warn', { aspects: ['invalidHref'] }], // Workaround for jsx-a11y/href-no-hash 52 | 'jsx-a11y/label-has-associated-control': [ 53 | 2, 54 | { 55 | // NOTE: If this error triggers, either disable it or add 56 | // your custom components, labels and attributes via these options 57 | // See https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/label-has-associated-control.md 58 | controlComponents: ['Input'], 59 | }, 60 | ], 61 | 'jsx-a11y/label-has-for': 0, 62 | 'jsx-a11y/mouse-events-have-key-events': 2, 63 | 'jsx-a11y/role-has-required-aria-props': 2, 64 | 'jsx-a11y/role-supports-aria-props': 2, 65 | 'linebreak-style': ['error', 'unix'], 66 | 'max-len': 0, 67 | 'newline-per-chained-call': 0, 68 | 'no-confusing-arrow': 0, 69 | 'no-console': 1, 70 | // 'no-param-reassign': [ 71 | // 2, 72 | // { 73 | // 'ignorePropertyModificationsFor': ['state'] 74 | // } 75 | // ], 76 | 'no-shadow': 2, // Only until we upgrade to react-boilerplate 5.0 77 | 'no-unused-vars': 2, 78 | 'no-use-before-define': 0, 79 | 'prefer-template': 2, 80 | 'react/destructuring-assignment': 0, 81 | 'react-hooks/rules-of-hooks': 'error', 82 | 'react/jsx-closing-tag-location': 0, 83 | 'react/forbid-prop-types': 0, 84 | 'react/jsx-first-prop-new-line': [2, 'multiline'], 85 | 'react/jsx-filename-extension': 0, 86 | 'react/jsx-fragments': [2, 'element'], 87 | 'react/jsx-props-no-spreading': 0, 88 | 'react/jsx-no-target-blank': 0, 89 | 'react/jsx-uses-vars': 2, // 1 90 | 'react/prefer-stateless-function': 1, 91 | 'react/require-default-props': 0, 92 | 'react/require-extension': 0, 93 | 'react/self-closing-comp': 0, 94 | 'react/sort-comp': 0, 95 | 'require-yield': 0, 96 | semi: ['error', 'never'], 97 | 'react/style-prop-object': 2, 98 | }, 99 | } 100 | --------------------------------------------------------------------------------