├── .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 |
--------------------------------------------------------------------------------