├── .gitignore ├── README.md ├── config ├── jest.config.js └── setupTests.ts ├── package.json ├── src ├── index.ts ├── useCookie.ts ├── useLocalStorage.ts ├── useNativeStorage.ts ├── useSessionStorage.ts ├── useStorage.ts └── utils.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Parcel 4 | .cache 5 | 6 | # dependencies 7 | /node_modules 8 | /.pnp 9 | .pnp.js 10 | 11 | # testing 12 | /coverage 13 | 14 | # production 15 | /dist 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # Using Yarn 29 | package-lock.json 30 | 31 | # Gitkeep files 32 | !.gitkeep 33 | 34 | # IDE config 35 | .idea 36 | *.iml 37 | /venv 38 | .vscode 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

useStorage

3 |

4 | 5 |
6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | undefined 19 | 20 | 21 | 24 | 27 | 30 | 33 | 36 |

37 | 38 |
39 | 40 | 🕋 React hook using local storage on SSR, CSR, and React Native apps 41 |
42 |
43 | ⚠ This is under active development. Stay tuned ⚠ 44 |
45 | 46 |
47 |
48 | 49 | 50 |
51 |
npm i use-react-storage
52 |
53 | 54 |
55 |
56 | 57 | Features 58 | --------- 59 | 60 | - SSR (server side rendering) support 61 | - TypeScript support 62 | - 1 dependency ([use-ssr](https://github.com/alex-cory/use-ssr)) 63 | - 🚫 React Native support 64 | - 🚫 [Ionic](https://ionicframework.com/docs/building/storage) suport 65 | - 🚫 Persistent Storage 66 | - 🚫 [localForge](https://github.com/localForage/localForage) support 67 | - 🚫 Server localStorage support ([redux-persist-node-storage](https://github.com/pellejacobs/redux-persist-node-storage)) 68 | 69 | Usage 70 | ------- 71 | 72 | ### Object Destructuring 73 | 74 | ```jsx 75 | import useStorage, { 76 | useLocalStorage, 77 | useCookie, // NOT IMPLEMENTED YET 78 | useSessionStorage, // NOT IMPLEMENTED YET 79 | useNativeStorage, // NOT IMPLEMENTED YET 80 | useIonicStorage, // NOT IMPLEMENTED YET 81 | useLocalForge, // NOT IMPLEMENTED YET 82 | useServerStorage. // NOT IMPLEMENTED YET 83 | } = 'use-react-storage' 84 | 85 | const App = () => { 86 | // SSR (server side rendered): cookies 87 | // CSR (client side rendered): localStorage, unless using `useSessionStorage` 88 | // Native (react native): AsyncStorage 89 | 90 | const { 91 | someKey1, 92 | someKey2, // can grab the `items/keys` right out 93 | set, // sets/overwrites the specified items 94 | merge, // merges the items 95 | has, 96 | remove, // removes the specified items 97 | clear, // clears the storage 98 | flushGetRequests, // NOT IMPLEMENTED YET (Native Only) 99 | allItems, // NOT IMPLEMENTED YET 100 | errors, // NOT IMPLEMENTED YET 101 | } = useStorage('someKey1', 'default-value-for-someKey1') 102 | 103 | // usages for `set` 104 | set({ someKey1: 'new value for someKey1' }) // for multi setting items 105 | set('someKey1', 'new value for someKey1') // for setting individual item 106 | // when the hook has a string argument such as useStorage('someKey1', 'default'), we assume if 107 | // `set` has 1 string argument like below, that it is setting 'someKey1' 108 | set('value for someKey1') 109 | 110 | // usages for `remove` 111 | remove('someKey1', 'someKey2') // would remove both items from storage 112 | 113 | // usages for `merge` 114 | merge('person[23eqad-person-id].name', 'Alex') // I think people will like this one more 115 | merge({ // this syntax is up in the air, might do it in a callbacky kinda way like useState's setState(x => ({ ...x })) 116 | person: { 117 | [23eqad-person-id]: { 118 | name: 'Alex' 119 | } 120 | } 121 | }) 122 | 123 | // OR 124 | const { 125 | someKey1, 126 | someKey2, 127 | set, 128 | has, 129 | merge, 130 | remove, 131 | clear, 132 | flushGetRequests, // NOT IMPLEMENTED YET (Native Only) 133 | allItems, // NOT IMPLEMENTED YET 134 | errors, // NOT IMPLEMENTED YET 135 | } = useStorage({ 136 | someKey1: 'default-1', 137 | someKey2: 'default-2' 138 | }) 139 | 140 | } 141 | ``` 142 | 143 | ### Array Destructuring 144 | 145 | ```js 146 | import useStorage from 'use-react-storage' 147 | 148 | const App = () => { 149 | const [token, setToken, removeToken] = useStorage('token', 'default-value-for-token') 150 | // used like 151 | setToken('the-new-token') 152 | removeItem() 153 | 154 | // OR 155 | const [items, set, remove] = useStorage({ 156 | item1: 'default-value-for-item-1', 157 | item2: 'default-value-for-item-2', 158 | }) 159 | const { item1, item2 } = items 160 | // used like 161 | set({ 162 | item1: 'no', 163 | item2: 'way' 164 | }) 165 | set('item1', 'new value for item1') 166 | remove('item1', 'item2') 167 | } 168 | ``` 169 | 170 | By default this will determine where your app is and decide which storage to use. 171 | If your app is SSR (server side rendered), a flag will be set and it will default to using `Cookies` for storage 172 | If your app is CSR (client side rendered), in the `browser` it will default to `localStorage` 173 | If your app is Native, it will default to [`AsyncStorage`](https://facebook.github.io/react-native/docs/asyncstorage) 174 | 175 | ### Others 176 | - [react-use-localstorage](https://github.com/dance2die/react-use-localstorage/blob/master/src/index.ts) 177 | - [react-use useSessionStorage](https://github.com/streamich/react-use/blob/master/docs/useSessionStorage.md) 178 | - [youtube nextjs - persist state with cookies](https://www.youtube.com/watch?v=_AYuhmz-fX4&t=0s) 179 | - [react-native-storage](https://github.com/sunnylqm/react-native-storage) 180 | - [@react-native-community/async-storage](https://github.com/react-native-community/async-storage) 181 | - [apollo-cache-persist](https://github.com/apollographql/apollo-cache-persist#storage-providers) 182 | -------------------------------------------------------------------------------- /config/jest.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const path = require('path') 3 | 4 | module.exports = { 5 | rootDir: process.cwd(), 6 | coverageDirectory: '/.coverage', 7 | globals: { 8 | __DEV__: true, 9 | }, 10 | collectCoverageFrom: [ 11 | 'src/**/*.{js,jsx,ts,tsx}', 12 | '!src/**/*.d.ts', 13 | '!src/**/*.test.*', 14 | '!src/test/**/*.*', 15 | ], 16 | setupFilesAfterEnv: [path.join(__dirname, './setupTests.ts')], 17 | testMatch: [ 18 | '/src/**/__tests__/**/*.ts?(x)', 19 | '/src/**/?(*.)(spec|test).ts?(x)', 20 | ], 21 | testEnvironment: 'node', 22 | testURL: 'http://localhost', 23 | transform: { 24 | '^.+\\.(ts|tsx)$': 'ts-jest', 25 | }, 26 | transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$'], 27 | testPathIgnorePatterns: ['/src/__tests__/test-utils.tsx'], 28 | moduleNameMapper: { 29 | '^react-native$': 'react-native-web', 30 | }, 31 | moduleFileExtensions: [ 32 | 'web.js', 33 | 'js', 34 | 'json', 35 | 'web.jsx', 36 | 'jsx', 37 | 'ts', 38 | 'tsx', 39 | 'feature', 40 | 'csv', 41 | ], 42 | } 43 | -------------------------------------------------------------------------------- /config/setupTests.ts: -------------------------------------------------------------------------------- 1 | 2 | // this is just a little hack to silence a warning that we'll get until react 3 | // fixes this: https://github.com/facebook/react/pull/14853 4 | const originalError = console.error 5 | beforeAll(() => { 6 | console.error = (...args: any[]) => { 7 | if (/Warning.*not wrapped in act/.test(args[0])) { 8 | return 9 | } 10 | originalError.call(console, ...args) 11 | } 12 | }) 13 | 14 | afterAll(() => { 15 | console.error = originalError 16 | }) 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-react-storage", 3 | "version": "0.0.1", 4 | "homepage": "https://github.com/alex-cory/use-react-storage", 5 | "main": "dist/index.js", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/alex-cory/use-storage.git" 10 | }, 11 | "dependencies": { 12 | "use-ssr": "^1.0.21" 13 | }, 14 | "peerDependencies": { 15 | "react": "^16.8.6", 16 | "react-dom": "^16.8.6" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^16.9.11", 20 | "@types/react-dom": "^16.9.4", 21 | "@testing-library/react": "^9.2.0", 22 | "@testing-library/react-hooks": "^2.0.2", 23 | "@types/fetch-mock": "^7.2.3", 24 | "@types/jest": "^24.0.12", 25 | "@types/node": "^12.0.10", 26 | "@typescript-eslint/eslint-plugin": "^2.0.0", 27 | "@typescript-eslint/parser": "^2.0.0", 28 | "convert-keys": "^1.3.4", 29 | "eslint": "^6.0.1", 30 | "eslint-config-prettier": "6.2.0", 31 | "eslint-plugin-jest": "22.16.0", 32 | "eslint-plugin-jest-formatting": "1.1.0", 33 | "eslint-plugin-jsx-a11y": "^6.2.1", 34 | "eslint-plugin-prettier": "^3.1.0", 35 | "eslint-plugin-react": "^7.14.2", 36 | "eslint-plugin-react-hooks": "^2.0.0", 37 | "eslint-plugin-sonarjs": "^0.4.0", 38 | "eslint-watch": "^5.1.2", 39 | "jest": "^24.7.1", 40 | "jest-fetch-mock": "^2.1.2", 41 | "prettier": "^1.18.2", 42 | "react": "^16.8.6", 43 | "react-dom": "^16.8.6", 44 | "react-hooks-testing-library": "^0.6.0", 45 | "react-test-renderer": "^16.8.6", 46 | "ts-jest": "^24.0.0", 47 | "typescript": "^3.4.5", 48 | "watch": "^1.0.2" 49 | }, 50 | "scripts": { 51 | "prepublishOnly": "yarn build # runs before publish", 52 | "build": "rm -rf dist && ./node_modules/.bin/tsc --module CommonJS", 53 | "build:watch": "rm -rf dist && tsc -w --module CommonJS", 54 | "tsc": "tsc -p . --noEmit && tsc -p ./src/__tests__", 55 | "test:browser": "yarn tsc && jest -c ./config/jest.config.js --env=jsdom", 56 | "test:browser:watch": "yarn tsc && jest --watch -c ./config/jest.config.js --env=jsdom", 57 | "test:server": "yarn tsc && jest -c ./config/jest.config.js --env=node", 58 | "test:server:watch": "yarn tsc && jest --watch -c ./config/jest.config.js --env=node", 59 | "test:watch": "yarn test:browser:watch && yarn test:server:watch", 60 | "test": "yarn test:browser && yarn test:server", 61 | "clean": "npm prune; yarn cache clean; rm -rf ./node_modules package-lock.json yarn.lock; yarn", 62 | "lint": "eslint ./src/**/*.{ts,tsx}", 63 | "lint:fix": "npm run lint -- --fix", 64 | "lint:watch": "watch 'yarn lint'" 65 | }, 66 | "files": [ 67 | "dist" 68 | ], 69 | "keywords": [ 70 | "use-session-storage", 71 | "use-local-storage", 72 | "localStorage", 73 | "react-use-localstorage", 74 | "use-localstorage", 75 | "use", 76 | "hook", 77 | "react", 78 | "react-hooks-fetch", 79 | "react localStorage", 80 | "react custom hooks", 81 | "use hooks", 82 | "react useLocalStorage hook", 83 | "use-storage", 84 | "useStorage" 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useStorage' 2 | export * from './useSessionStorage' 3 | export * from './useLocalStorage' 4 | export * from './useNativeStorage' 5 | export * from './useCookie' -------------------------------------------------------------------------------- /src/useCookie.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/js-cookie/js-cookie/blob/master/src/js.cookie.mjs 2 | 3 | /** 4 | * TODO 5 | */ 6 | // to get all the cookies in an object 7 | // document.cookie.split('; ').reduce((prev, current) => { 8 | // const [name, value] = current.split('='); 9 | // prev[name] = value; 10 | // return prev 11 | // }, {}); 12 | export const useCookie = (objOrString?: {} | string, str?: string): {} => ({}) -------------------------------------------------------------------------------- /src/useLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from 'react' 2 | import { tryToParse, isObject, isString } from './utils' 3 | // import useSSR from 'use-ssr' 4 | 5 | export function useLocalStorage( 6 | keyOrObj?: any, 7 | defaultVal?: string 8 | ) { 9 | const parseStorage = useCallback((storage: {}) => { 10 | return Object.entries(storage).reduce((acc, [key, val]) => { 11 | if (isString(keyOrObj) && keyOrObj === key && !val) { 12 | (acc as any)[key] = defaultVal 13 | } else { 14 | (acc as any)[key] = tryToParse(val as string) 15 | } 16 | return acc 17 | }, {}) 18 | }, [isString, keyOrObj, tryToParse, defaultVal]) 19 | 20 | const [storageObj, setStorageObj] = useState(() => parseStorage(localStorage)) 21 | 22 | const updateStorageObj = useCallback(() => setStorageObj(parseStorage(localStorage)), [setStorageObj, parseStorage]) 23 | 24 | const set = useCallback((keyOrObj: string | {}, val?: string) => { 25 | if (isObject(keyOrObj)) { 26 | Object.entries(keyOrObj).forEach(([k, v]) => { 27 | localStorage.setItem(k, v as string) 28 | }) 29 | } else { 30 | if (!val) throw Error('must have a value. i.e. `set("key", "value")`') 31 | localStorage.setItem(keyOrObj, val) 32 | } 33 | updateStorageObj() 34 | }, [updateStorageObj]) 35 | 36 | const remove = useCallback((...keys) => { 37 | if (keys.length > 1) { 38 | keys.forEach(key => localStorage.removeItem(key)) 39 | } else { 40 | if (keys.length === 0) throw Error('must have a key to remove. i.e. `remove("key1", "key2")`') 41 | localStorage.removeItem(keys[0]) 42 | } 43 | updateStorageObj() 44 | }, [updateStorageObj]) 45 | 46 | const clear = useCallback(() => { 47 | localStorage.clear() 48 | updateStorageObj() 49 | }, [updateStorageObj]) 50 | 51 | const value = isString(keyOrObj) ? (storageObj as any)[keyOrObj] : storageObj 52 | const setArr = (v: any) => isString(keyOrObj) ? set(keyOrObj, v) : set(v as any) 53 | const removeArr = (v?: string) => isString(keyOrObj) ? remove(keyOrObj) : remove(v) 54 | 55 | return Object.assign([value, setArr, removeArr], { 56 | ...storageObj, 57 | set, 58 | remove, 59 | clear, 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /src/useNativeStorage.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/react-native-community/async-storage/blob/LEGACY/docs/API.md#getAllKeys 2 | // import AsyncStorage from '@react-native-community/async-storage' 3 | import { useState, useCallback } from 'react' 4 | import { tryToParse, isObject, isString } from './utils' 5 | 6 | 7 | /** 8 | * TODO: 9 | * THIS DOES NOT CURRENTLY WORK 10 | */ 11 | export function useNativeStorage( 12 | keyOrObj?: any, 13 | defaultVal?: string 14 | ) { 15 | const parseStorage = useCallback((storage: {}) => { 16 | return Object.entries(storage).reduce((acc, [key, val]) => { 17 | if (isString(keyOrObj) && keyOrObj === key && !val) { 18 | (acc as any)[key] = defaultVal 19 | } else { 20 | (acc as any)[key] = tryToParse(val as string) 21 | } 22 | return acc 23 | }, {}) 24 | }, [isString, keyOrObj, tryToParse, defaultVal]) 25 | 26 | const [storageObj, setStorageObj] = useState(() => parseStorage(localStorage)) 27 | 28 | const updateStorageObj = useCallback(() => { 29 | console.warn('THIS IS NOT IMPLEMENTED YET') 30 | setStorageObj(parseStorage(localStorage)) 31 | }, [setStorageObj, parseStorage]) 32 | 33 | 34 | const set = useCallback((keyOrObj: string | {}, val?: string) => { 35 | if (isObject(keyOrObj)) { 36 | Object.entries(keyOrObj).forEach(([k, v]) => { 37 | localStorage.setItem(k, v as string) 38 | }) 39 | } else { 40 | if (!val) throw Error('must have a value. i.e. `set("key", "value")`') 41 | localStorage.setItem(keyOrObj, val) 42 | } 43 | updateStorageObj() 44 | }, [updateStorageObj]) 45 | 46 | const remove = useCallback((...keys) => { 47 | if (keys.length > 1) { 48 | keys.forEach(key => localStorage.removeItem(key)) 49 | } else { 50 | if (keys.length === 0) throw Error('must have a key to remove. i.e. `remove("key1", "key2")`') 51 | localStorage.removeItem(keys[0]) 52 | } 53 | updateStorageObj() 54 | }, [updateStorageObj]) 55 | 56 | const clear = useCallback(() => { 57 | localStorage.clear() 58 | updateStorageObj() 59 | }, [updateStorageObj]) 60 | 61 | const value = isString(keyOrObj) ? (storageObj as any)[keyOrObj] : storageObj 62 | const setArr = (v: any) => isString(keyOrObj) ? set(keyOrObj, v) : set(v as any) 63 | const removeArr = (v?: string) => isString(keyOrObj) ? remove(keyOrObj) : remove(v) 64 | 65 | return Object.assign([value, setArr, removeArr], { 66 | ...storageObj, 67 | set, 68 | remove, 69 | clear, 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /src/useSessionStorage.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import useSSR from 'use-ssr' 3 | 4 | 5 | /** 6 | * TODO 7 | * THIS DOES NOT CURRENTLY WORK 8 | */ 9 | export const useSessionStorage = (key: string, initialValue?: T, raw?: boolean): [T, (value: T) => void] => { 10 | const { isServer, isNative } = useSSR() 11 | if (isServer || isNative) { 12 | return [initialValue as T, () => {}]; 13 | } 14 | 15 | const [state, setState] = useState(() => { 16 | try { 17 | const sessionStorageValue = sessionStorage.getItem(key); 18 | if (typeof sessionStorageValue !== 'string') { 19 | sessionStorage.setItem(key, raw ? String(initialValue) : JSON.stringify(initialValue)); 20 | return initialValue; 21 | } else { 22 | return raw ? sessionStorageValue : JSON.parse(sessionStorageValue || 'null'); 23 | } 24 | } catch { 25 | // If user is in private mode or has storage restriction 26 | // sessionStorage can throw. JSON.parse and JSON.stringify 27 | // cat throw, too. 28 | return initialValue; 29 | } 30 | }); 31 | 32 | useEffect(() => { 33 | try { 34 | const serializedState = raw ? String(state) : JSON.stringify(state); 35 | sessionStorage.setItem(key, serializedState); 36 | } catch { 37 | // If user is in private mode or has storage restriction 38 | // sessionStorage can throw. Also JSON.stringify can throw. 39 | } 40 | }); 41 | 42 | return [state, setState]; 43 | }; 44 | 45 | export default useSessionStorage; -------------------------------------------------------------------------------- /src/useStorage.ts: -------------------------------------------------------------------------------- 1 | import useSSR from 'use-ssr' 2 | import { useNativeStorage, useLocalStorage, useCookie } from '.' 3 | 4 | 5 | type StorageArgs = [({} | string)?, string?] 6 | 7 | export const useStorage = (...args: StorageArgs) => { 8 | let { device } = useSSR() 9 | 10 | const hooks = { 11 | native: () => { 12 | console.warn('NOT IMPLEMENTED YET') 13 | return useNativeStorage(...args) 14 | }, 15 | browser: useLocalStorage(...args), 16 | server: () => { 17 | console.warn('NOT IMPLEMENTED YET') 18 | return useCookie(...args) 19 | }, 20 | } 21 | 22 | return (hooks as any)[device] 23 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export const isString = (x: any) => typeof x === 'string' 4 | 5 | /** 6 | * Determines if the given param is an object. {} 7 | * @param obj 8 | */ 9 | export const isObject = (obj: any): obj is object => Object.prototype.toString.call(obj) === '[object Object]' // eslint-disable-line 10 | 11 | export function tryToParse(str: string) { 12 | try { 13 | return JSON.parse(str) 14 | } catch (err) { 15 | return str 16 | } 17 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "jsx": "react", 7 | "lib": [ 8 | "es5", 9 | "es2015", 10 | "es2016", 11 | "dom", 12 | "esnext" 13 | ], 14 | "module": "es2015", 15 | "moduleResolution": "node", 16 | "noImplicitAny": true, 17 | "noUnusedLocals": true, 18 | "outDir": "dist", 19 | "sourceMap": true, 20 | "strict": true, 21 | "target": "es6" 22 | }, 23 | "include": [ 24 | "src/*.ts" 25 | ], 26 | "exclude": [ 27 | "node_modules" 28 | ] 29 | } --------------------------------------------------------------------------------