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