├── .gitignore ├── README.md ├── babel.config.js ├── dist ├── createAsyncStorage.js ├── createMigrate.js ├── createPersistContext.js ├── createSensitiveStorage.js ├── defaultOptions.js ├── index.js ├── usePersistStorage.js └── utils.js ├── package.json ├── src ├── createAsyncStorage.ts ├── createMigrate.ts ├── createPersistContext.tsx ├── createSensitiveStorage.ts ├── defaultOptions.ts ├── index.ts ├── usePersistStorage.ts └── utils.ts ├── tests ├── __mocks__ │ ├── @react-native-community │ │ └── async-storage.js │ └── react-native-sensitive-info.js ├── createAsyncStorage.test.ts ├── createMigrate.test.ts ├── setup.ts ├── usePersistStorage.test.ts └── utils.ts ├── tsconfig.json ├── tslint.json ├── types ├── createAsyncStorage.d.ts ├── createMigrate.d.ts ├── createPersistContext.d.ts ├── createSensitiveStorage.d.ts ├── defaultOptions.d.ts ├── index.d.ts ├── usePersistStorage.d.ts └── utils.d.ts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | .DS_Store 9 | .env 10 | build 11 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Hooks - usePersistStorage 2 | 3 | Persist and rehydrate a **context store** by React Hooks 4 | 5 | - An asynchronous persist storage 6 | - Support **sensitive info** both on iOS & Android 7 | - Migration function 8 | 9 | [![npm version](https://badge.fury.io/js/react-native-use-persist-storage.svg)](https://badge.fury.io/js/react-native-use-persist-storage) 10 | 11 | ## Install 12 | 13 | Install react-native-use-persist-storage 14 | 15 | ``` 16 | $ yarn add react-native-use-persist-storage 17 | ``` 18 | 19 | Install **@react-native-async-storage/async-storage**, **react-native-sensitive-info** 20 | (see [peerDependencies](https://github.com/visuallylab/react-native-use-persist-storage#peer-dependencies)) 21 | 22 | ``` 23 | $ yarn add react-native-sensitive-info @react-native-async-storage/async-storage 24 | ``` 25 | 26 | If RN < 0.60, link dependencies 27 | 28 | ``` 29 | $ react-native link react-native-sensitive-info 30 | $ react-native link @react-native-async-storage/async-storage 31 | ``` 32 | 33 | Note: For IOS project using pods, run: \$ cd ios/ && pod install 34 | 35 | #### Peer Dependencies 36 | 37 | **Note:** `react-native-use-persist-storage` requires the following peer dependencies: 38 | 39 | - react >= 16.8.1 40 | - react-native >= 0.59.0 41 | - [react-native-sensitive-info](https://github.com/mCodex/react-native-sensitive-info) 42 | - [@react-native-async-storage/async-storage](https://github.com/react-native-async-storage/async-storage) 43 | 44 | --- 45 | 46 | ## Usage 47 | 48 | Make sure you know how [React Hooks](https://reactjs.org/docs/hooks-reference.html) works, and you can use it just same as useState. 49 | 50 | #### Basic Usage 51 | 52 | ```js 53 | // User.js 54 | import { usePersistStorage } from "react-native-use-persist-storage"; 55 | 56 | const createDefaultUser = () => ({ 57 | name: "" 58 | }); 59 | 60 | const User = props => { 61 | const [user, setUser, restored] = usePersistStorage( 62 | "@User", 63 | createDefaultUser 64 | ); 65 | if (restored) return loading...; 66 | return {user.name}; 67 | }; 68 | ``` 69 | 70 | #### Context Usage 71 | 72 | If you want a light-weight global state management solution in react, try using [Context](https://reactjs.org/docs/context.html). 73 | 74 | ```js 75 | // contexts/user.js 76 | import { createContext } from 'react' 77 | import { usePersistStorage } from 'react-native-use-persist-storage' 78 | 79 | const createDefaultUser = () => ({ 80 | name: '', 81 | }); 82 | 83 | const UserContext = createContext(...); 84 | 85 | const UserProvider = props => { 86 | const [user, setUser, restored] = usePersistStorage('@User', createDefaultUser); 87 | 88 | // anything you need... 89 | 90 | return ( 91 | 96 | {props.children} 97 | 98 | ); 99 | }; 100 | ``` 101 | 102 | ```js 103 | // GlobalStateProvider.ts 104 | const GlobalStateProvider = ({ children }) => ( 105 | 106 | 107 | {children} 108 | 109 | 110 | ) 111 | 112 | // App.js 113 | const App = () => { 114 | return ( 115 | 116 | 117 | 118 | ); 119 | }; 120 | ``` 121 | 122 | #### Recommend use: [createPersistContext](#createPersistContext) 123 | 124 | ## API 125 | 126 | #### `usePersistStorage(key, initialValue, options?)` 127 | 128 | - arguments 129 | - `key`: async storage key 130 | - `initialValue`: initial value 131 | - `options`: set options `debug`, `persist`, `version`, `migrate`, `sensitive`. 132 | 133 | ##### `options` ([see](https://github.com/visuallylab/react-native-use-persist-storage/blob/master/src/defaultOptions.ts#L4)) 134 | 135 | - `debug`: call `console.debug` when any persist storage action. 136 | - default: `false` 137 | - `persist`: set false, it will same as useState 138 | - default: `true` 139 | - `version`: storage version, set with migrate function 140 | - default: `0` 141 | - `migrate`: set migrate function, [see how to use createMigrate](#createMigrate) 142 | - default: `null` 143 | - `sensitive`: pass [config options](https://mcodex.dev/react-native-sensitive-info/docs/5.x/ios_options), it will use [react-native-sensitive-info](https://github.com/mCodex/react-native-sensitive-info) to store your data. 144 | - default: `false` 145 | 146 | #### `createPersistContext` 147 | 148 | It is a simple built-in util function for easy use. [See](https://github.com/visuallylab/react-native-use-persist-storage/blob/master/src/createPersistContext.tsx) 149 | 150 | ```js 151 | // contexts/user.js 152 | import { createPersistContext } from 'react-native-use-persist-storage'; 153 | 154 | export default createPersistContext({ 155 | storageKey: '@User', 156 | defaultData: { 157 | name: 'Awesome!' 158 | }, 159 | options: { ... } 160 | }); 161 | 162 | // App.js 163 | import User from './contexts/user'; 164 | const App = () => { 165 | return ( 166 | 167 | 168 | 169 | ); 170 | }; 171 | 172 | // Component.js 173 | import User from './contexts/user'; 174 | 175 | const Component = () => { 176 | const [user, setUser] = User.useData(); 177 | return ... 178 | }; 179 | 180 | ``` 181 | 182 | **Or, you can create a hook to customize** 183 | 184 | ```js 185 | // hooks/useUser.js 186 | import User from './contexts/user'; 187 | 188 | const useUser = () => { 189 | const [user, setUser] = useContext(User.Context); 190 | const setName = () => ... 191 | const setEmail = () => ... 192 | return { 193 | user, 194 | setName, 195 | setEmail, 196 | setUser, 197 | // anything ... 198 | }; 199 | }; 200 | 201 | const Component = () => { 202 | const { user, ... } = useUser(); 203 | ... 204 | } 205 | 206 | ``` 207 | 208 | #### `createMigrate` 209 | 210 | Use like [redux migration](https://github.com/rt2zz/redux-persist/blob/master/docs/migrations.md) 211 | 212 | ```js 213 | import { createMigrate, usePersistStorage } from 'react-native-use-persist-storage'; 214 | 215 | const [user, setUser, restored] = usePersistStorage( 216 | '@User', 217 | defaultUser, 218 | { 219 | version: 2, 220 | migrate: createMigrate( 221 | { 222 | 1: state => { 223 | // ... 224 | }, 225 | 2: state => ({ 226 | // ... 227 | }), 228 | 229 | }, 230 | ), 231 | }, 232 | ) 233 | 234 | ``` 235 | 236 | #### `setPersistStorageDefaults` 237 | You can control all default options when app initialized.([see](https://github.com/visuallylab/react-native-use-persist-storage/blob/master/src/defaultOptions.ts#L4)) 238 | 239 | - open debug log: 240 | ```js 241 | setPersistStorageDefaults({ debug: true }); 242 | ``` 243 | 244 | - remove all stored values in persistent storage: 245 | ```js 246 | setPersistStorageDefaults({ persist: false }); 247 | ``` 248 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // babel.config.js 2 | module.exports = { 3 | presets: [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | targets: { 8 | node: "current" 9 | } 10 | } 11 | ], 12 | "@babel/preset-typescript" 13 | ], 14 | "plugins": ["transform-flow-strip-types"] 15 | }; 16 | -------------------------------------------------------------------------------- /dist/createAsyncStorage.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | var __importDefault = (this && this.__importDefault) || function (mod) { 39 | return (mod && mod.__esModule) ? mod : { "default": mod }; 40 | }; 41 | Object.defineProperty(exports, "__esModule", { value: true }); 42 | var async_storage_1 = __importDefault(require("@react-native-async-storage/async-storage")); 43 | var noop = function () { return null; }; 44 | var createAsyncStorage = function () { 45 | return { 46 | getItem: function (key, callback) { 47 | if (callback === void 0) { callback = noop; } 48 | return __awaiter(this, void 0, void 0, function () { 49 | var result, error_1; 50 | return __generator(this, function (_a) { 51 | switch (_a.label) { 52 | case 0: 53 | _a.trys.push([0, 2, , 3]); 54 | return [4, async_storage_1.default.getItem(key)]; 55 | case 1: 56 | result = _a.sent(); 57 | callback(null, result); 58 | return [2, result]; 59 | case 2: 60 | error_1 = _a.sent(); 61 | callback(error_1, null); 62 | throw error_1; 63 | case 3: return [2]; 64 | } 65 | }); 66 | }); 67 | }, 68 | setItem: function (key, value, callback) { 69 | if (callback === void 0) { callback = noop; } 70 | return __awaiter(this, void 0, void 0, function () { 71 | var error_2; 72 | return __generator(this, function (_a) { 73 | switch (_a.label) { 74 | case 0: 75 | _a.trys.push([0, 2, , 3]); 76 | return [4, async_storage_1.default.setItem(key, value)]; 77 | case 1: 78 | _a.sent(); 79 | callback(null, value); 80 | return [3, 3]; 81 | case 2: 82 | error_2 = _a.sent(); 83 | callback(error_2, null); 84 | throw error_2; 85 | case 3: return [2]; 86 | } 87 | }); 88 | }); 89 | }, 90 | removeItem: function (key, callback) { 91 | if (callback === void 0) { callback = noop; } 92 | return __awaiter(this, void 0, void 0, function () { 93 | var error_3; 94 | return __generator(this, function (_a) { 95 | switch (_a.label) { 96 | case 0: 97 | _a.trys.push([0, 2, , 3]); 98 | return [4, async_storage_1.default.removeItem(key)]; 99 | case 1: 100 | _a.sent(); 101 | callback(null, null); 102 | return [3, 3]; 103 | case 2: 104 | error_3 = _a.sent(); 105 | callback(error_3, null); 106 | throw error_3; 107 | case 3: return [2]; 108 | } 109 | }); 110 | }); 111 | }, 112 | }; 113 | }; 114 | exports.default = createAsyncStorage; 115 | -------------------------------------------------------------------------------- /dist/createMigrate.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var defaultOptions_1 = require("./defaultOptions"); 4 | var utils_1 = require("./utils"); 5 | var createMigrate = function (migrations, configs) { 6 | if (configs === void 0) { configs = { debug: defaultOptions_1.defaultOptions.debug }; } 7 | return function (_a) { 8 | var key = _a.key, state = _a.state, version = _a.version; 9 | var debug = configs.debug; 10 | if (!state.value) { 11 | if (debug) { 12 | console.debug("[" + key + "]: no inbound value, skipping migration"); 13 | } 14 | return state; 15 | } 16 | if (state._currentVersion === version) { 17 | if (debug) { 18 | console.debug("[" + key + "]: version match, no migration"); 19 | } 20 | return state; 21 | } 22 | if (state._currentVersion > version) { 23 | console.warn("[" + key + "]: downgrading version is not supported"); 24 | return state; 25 | } 26 | var migrationKeys = Object.keys(migrations) 27 | .map(function (v) { return parseInt(v, 10); }) 28 | .filter(function (ver) { return version >= ver && ver > state._currentVersion; }) 29 | .sort(function (a, b) { return a - b; }); 30 | if (debug) { 31 | console.debug("[" + key + "]: migration keys", migrationKeys); 32 | } 33 | var migrated = migrationKeys.reduce(function (_a, versionKey) { 34 | var value = _a.value; 35 | if (debug) { 36 | console.warn("[" + key + "]: running migration " + versionKey); 37 | } 38 | return utils_1.transformStorageValue(migrations[versionKey](value), versionKey); 39 | }, state); 40 | return migrated; 41 | }; 42 | }; 43 | exports.default = createMigrate; 44 | -------------------------------------------------------------------------------- /dist/createPersistContext.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __assign = (this && this.__assign) || function () { 3 | __assign = Object.assign || function(t) { 4 | for (var s, i = 1, n = arguments.length; i < n; i++) { 5 | s = arguments[i]; 6 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 7 | t[p] = s[p]; 8 | } 9 | return t; 10 | }; 11 | return __assign.apply(this, arguments); 12 | }; 13 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 14 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 15 | return new (P || (P = Promise))(function (resolve, reject) { 16 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 17 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 18 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 19 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 20 | }); 21 | }; 22 | var __generator = (this && this.__generator) || function (thisArg, body) { 23 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 24 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 25 | function verb(n) { return function (v) { return step([n, v]); }; } 26 | function step(op) { 27 | if (f) throw new TypeError("Generator is already executing."); 28 | while (_) try { 29 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 30 | if (y = 0, t) op = [op[0] & 2, t.value]; 31 | switch (op[0]) { 32 | case 0: case 1: t = op; break; 33 | case 4: _.label++; return { value: op[1], done: false }; 34 | case 5: _.label++; y = op[1]; op = [0]; continue; 35 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 36 | default: 37 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 38 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 39 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 40 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 41 | if (t[2]) _.ops.pop(); 42 | _.trys.pop(); continue; 43 | } 44 | op = body.call(thisArg, _); 45 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 46 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 47 | } 48 | }; 49 | var __importDefault = (this && this.__importDefault) || function (mod) { 50 | return (mod && mod.__esModule) ? mod : { "default": mod }; 51 | }; 52 | Object.defineProperty(exports, "__esModule", { value: true }); 53 | var react_1 = __importDefault(require("react")); 54 | var usePersistStorage_1 = __importDefault(require("./usePersistStorage")); 55 | var createPersistContext = function (_a) { 56 | var storageKey = _a.storageKey, defaultData = _a.defaultData, options = _a.options; 57 | var createDefaultData = function () { return defaultData; }; 58 | var Context = react_1.default.createContext([ 59 | createDefaultData(), 60 | function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { 61 | ; 62 | return [2]; 63 | }); }); }, 64 | false 65 | ]); 66 | var Provider = function (props) { 67 | var _a = usePersistStorage_1.default(storageKey, createDefaultData, __assign({ persist: props.persist }, options)), data = _a[0], setData = _a[1], restored = _a[2]; 68 | return (react_1.default.createElement(Context.Provider, { value: [data, setData, restored] }, props.children)); 69 | }; 70 | var useData = function () { 71 | var context = react_1.default.useContext(Context); 72 | if (!context) { 73 | throw new Error("Context error: context [" + storageKey + "] must be used within a Provider"); 74 | } 75 | return context; 76 | }; 77 | return { 78 | Provider: Provider, 79 | Context: Context, 80 | useData: useData 81 | }; 82 | }; 83 | exports.default = createPersistContext; 84 | -------------------------------------------------------------------------------- /dist/createSensitiveStorage.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | var __importDefault = (this && this.__importDefault) || function (mod) { 39 | return (mod && mod.__esModule) ? mod : { "default": mod }; 40 | }; 41 | Object.defineProperty(exports, "__esModule", { value: true }); 42 | var react_native_1 = require("react-native"); 43 | var react_native_sensitive_info_1 = __importDefault(require("react-native-sensitive-info")); 44 | var extractKeys = react_native_1.Platform.select({ 45 | ios: function (items) { return items[0].map(function (item) { return item.key; }); }, 46 | android: Object.keys, 47 | }); 48 | var noop = function () { return null; }; 49 | var createSensitiveStorage = function (sensitiveOpts) { 50 | if (sensitiveOpts === void 0) { sensitiveOpts = {}; } 51 | return { 52 | getItem: function (key, callback) { 53 | if (callback === void 0) { callback = noop; } 54 | return __awaiter(this, void 0, void 0, function () { 55 | var result, error_1; 56 | return __generator(this, function (_a) { 57 | switch (_a.label) { 58 | case 0: 59 | _a.trys.push([0, 2, , 3]); 60 | return [4, react_native_sensitive_info_1.default.getItem(key, sensitiveOpts)]; 61 | case 1: 62 | result = _a.sent(); 63 | if (typeof result === 'undefined') { 64 | result = null; 65 | } 66 | callback(null, result); 67 | return [2, result]; 68 | case 2: 69 | error_1 = _a.sent(); 70 | callback(error_1, null); 71 | throw error_1; 72 | case 3: return [2]; 73 | } 74 | }); 75 | }); 76 | }, 77 | setItem: function (key, value, callback) { 78 | if (callback === void 0) { callback = noop; } 79 | return __awaiter(this, void 0, void 0, function () { 80 | var error_2; 81 | return __generator(this, function (_a) { 82 | switch (_a.label) { 83 | case 0: 84 | _a.trys.push([0, 2, , 3]); 85 | return [4, react_native_sensitive_info_1.default.setItem(key, value, sensitiveOpts)]; 86 | case 1: 87 | _a.sent(); 88 | callback(null, value); 89 | return [3, 3]; 90 | case 2: 91 | error_2 = _a.sent(); 92 | callback(error_2, null); 93 | throw error_2; 94 | case 3: return [2]; 95 | } 96 | }); 97 | }); 98 | }, 99 | removeItem: function (key, callback) { 100 | if (callback === void 0) { callback = noop; } 101 | return __awaiter(this, void 0, void 0, function () { 102 | var error_3; 103 | return __generator(this, function (_a) { 104 | switch (_a.label) { 105 | case 0: 106 | _a.trys.push([0, 2, , 3]); 107 | return [4, react_native_sensitive_info_1.default.deleteItem(key, sensitiveOpts)]; 108 | case 1: 109 | _a.sent(); 110 | callback(null, null); 111 | return [3, 3]; 112 | case 2: 113 | error_3 = _a.sent(); 114 | callback(error_3, null); 115 | throw error_3; 116 | case 3: return [2]; 117 | } 118 | }); 119 | }); 120 | }, 121 | getAllKeys: function (callback) { 122 | if (callback === void 0) { callback = noop; } 123 | return __awaiter(this, void 0, void 0, function () { 124 | var values, result, error_4; 125 | return __generator(this, function (_a) { 126 | switch (_a.label) { 127 | case 0: 128 | _a.trys.push([0, 2, , 3]); 129 | return [4, react_native_sensitive_info_1.default.getAllItems(sensitiveOpts)]; 130 | case 1: 131 | values = (_a.sent()); 132 | result = extractKeys(values); 133 | callback(null, result); 134 | return [2, result]; 135 | case 2: 136 | error_4 = _a.sent(); 137 | callback(error_4, null); 138 | throw error_4; 139 | case 3: return [2]; 140 | } 141 | }); 142 | }); 143 | }, 144 | }; 145 | }; 146 | exports.default = createSensitiveStorage; 147 | -------------------------------------------------------------------------------- /dist/defaultOptions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __assign = (this && this.__assign) || function () { 3 | __assign = Object.assign || function(t) { 4 | for (var s, i = 1, n = arguments.length; i < n; i++) { 5 | s = arguments[i]; 6 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 7 | t[p] = s[p]; 8 | } 9 | return t; 10 | }; 11 | return __assign.apply(this, arguments); 12 | }; 13 | Object.defineProperty(exports, "__esModule", { value: true }); 14 | exports.defaultOptions = { 15 | debug: false, 16 | version: 0, 17 | persist: true, 18 | migrate: null, 19 | sensitive: false, 20 | }; 21 | exports.setPersistStorageDefaults = function (configs) { 22 | exports.defaultOptions = __assign(__assign({}, exports.defaultOptions), configs); 23 | }; 24 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function __export(m) { 3 | for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; 4 | } 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | __export(require("./usePersistStorage")); 7 | var usePersistStorage_1 = require("./usePersistStorage"); 8 | exports.usePersistStorage = usePersistStorage_1.default; 9 | __export(require("./createPersistContext")); 10 | var createPersistContext_1 = require("./createPersistContext"); 11 | exports.createPersistContext = createPersistContext_1.default; 12 | __export(require("./createMigrate")); 13 | var createMigrate_1 = require("./createMigrate"); 14 | exports.createMigrate = createMigrate_1.default; 15 | var defaultOptions_1 = require("./defaultOptions"); 16 | exports.setPersistStorageDefaults = defaultOptions_1.setPersistStorageDefaults; 17 | -------------------------------------------------------------------------------- /dist/usePersistStorage.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | var __importDefault = (this && this.__importDefault) || function (mod) { 39 | return (mod && mod.__esModule) ? mod : { "default": mod }; 40 | }; 41 | Object.defineProperty(exports, "__esModule", { value: true }); 42 | var react_1 = require("react"); 43 | var createAsyncStorage_1 = __importDefault(require("./createAsyncStorage")); 44 | var createSensitiveStorage_1 = __importDefault(require("./createSensitiveStorage")); 45 | var defaultOptions_1 = require("./defaultOptions"); 46 | var utils_1 = require("./utils"); 47 | var usePersistStorage = function (key, initialValue, _a) { 48 | var _b = _a === void 0 ? defaultOptions_1.defaultOptions : _a, _c = _b.debug, debug = _c === void 0 ? defaultOptions_1.defaultOptions.debug : _c, _d = _b.version, version = _d === void 0 ? defaultOptions_1.defaultOptions.version : _d, _e = _b.persist, persist = _e === void 0 ? defaultOptions_1.defaultOptions.persist : _e, _f = _b.migrate, migrate = _f === void 0 ? defaultOptions_1.defaultOptions.migrate : _f, _g = _b.sensitive, sensitive = _g === void 0 ? defaultOptions_1.defaultOptions.sensitive : _g; 49 | var currentVersion = react_1.useRef(version || 0); 50 | var _h = react_1.useState(initialValue), state = _h[0], setState = _h[1]; 51 | var _j = react_1.useState(false), restored = _j[0], setRestored = _j[1]; 52 | var Storage = react_1.useMemo(function () { 53 | return sensitive ? createSensitiveStorage_1.default(sensitive) : createAsyncStorage_1.default(); 54 | }, [sensitive]); 55 | var logPrefix = sensitive ? "(sensitive)" : ""; 56 | var setValueToStorage = react_1.useCallback(function (newValue) { return __awaiter(void 0, void 0, void 0, function () { 57 | var value, serializedValue, err_1; 58 | return __generator(this, function (_a) { 59 | switch (_a.label) { 60 | case 0: 61 | value = utils_1.transformStorageValue(newValue, currentVersion.current); 62 | _a.label = 1; 63 | case 1: 64 | _a.trys.push([1, 3, , 4]); 65 | serializedValue = JSON.stringify(value); 66 | return [4, Storage.setItem(key, serializedValue)]; 67 | case 2: 68 | _a.sent(); 69 | if (debug) { 70 | console.debug(logPrefix + "[PersistStorage]: set " + key + ": ", value); 71 | } 72 | return [3, 4]; 73 | case 3: 74 | err_1 = _a.sent(); 75 | console.error(err_1); 76 | return [3, 4]; 77 | case 4: return [2]; 78 | } 79 | }); 80 | }); }, [Storage]); 81 | react_1.useEffect(function () { 82 | if (persist) { 83 | var restoreStateFromStorage = function () { return __awaiter(void 0, void 0, void 0, function () { 84 | var storageValue, parsedValue, err_2; 85 | return __generator(this, function (_a) { 86 | switch (_a.label) { 87 | case 0: 88 | _a.trys.push([0, 7, , 8]); 89 | return [4, Storage.getItem(key)]; 90 | case 1: 91 | storageValue = _a.sent(); 92 | if (!storageValue) return [3, 4]; 93 | parsedValue = JSON.parse(storageValue || "null"); 94 | if (parsedValue && parsedValue._currentVersion === undefined) { 95 | parsedValue = utils_1.transformStorageValue(parsedValue, currentVersion.current); 96 | } 97 | if (!migrate) return [3, 3]; 98 | parsedValue = migrate({ 99 | key: key, 100 | state: parsedValue, 101 | version: currentVersion.current 102 | }); 103 | currentVersion.current = parsedValue._currentVersion; 104 | return [4, setValueToStorage(parsedValue.value)]; 105 | case 2: 106 | _a.sent(); 107 | _a.label = 3; 108 | case 3: 109 | setState(parsedValue.value); 110 | if (debug) { 111 | console.debug(logPrefix + "[PersistStorage]: restore " + key + ": ", parsedValue); 112 | } 113 | return [3, 6]; 114 | case 4: return [4, setValueToStorage(state)]; 115 | case 5: 116 | _a.sent(); 117 | _a.label = 6; 118 | case 6: return [3, 8]; 119 | case 7: 120 | err_2 = _a.sent(); 121 | console.error(err_2); 122 | return [3, 8]; 123 | case 8: 124 | setRestored(true); 125 | return [2]; 126 | } 127 | }); 128 | }); }; 129 | restoreStateFromStorage(); 130 | } 131 | else { 132 | var removePersistItem = function () { return __awaiter(void 0, void 0, void 0, function () { 133 | return __generator(this, function (_a) { 134 | switch (_a.label) { 135 | case 0: return [4, Storage.removeItem(key)]; 136 | case 1: 137 | _a.sent(); 138 | setRestored(true); 139 | return [2]; 140 | } 141 | }); 142 | }); }; 143 | removePersistItem(); 144 | if (debug) { 145 | console.debug(logPrefix + "[PersistStorage]: remove " + key); 146 | } 147 | } 148 | }, []); 149 | var asyncSetState = function (stateOrCallbackFn) { return __awaiter(void 0, void 0, void 0, function () { 150 | var newValue; 151 | return __generator(this, function (_a) { 152 | switch (_a.label) { 153 | case 0: 154 | newValue = stateOrCallbackFn instanceof Function 155 | ? stateOrCallbackFn(state) 156 | : stateOrCallbackFn; 157 | setState(newValue); 158 | if (!persist) return [3, 2]; 159 | return [4, setValueToStorage(newValue)]; 160 | case 1: 161 | _a.sent(); 162 | _a.label = 2; 163 | case 2: return [2]; 164 | } 165 | }); 166 | }); }; 167 | return [state, asyncSetState, restored]; 168 | }; 169 | exports.default = usePersistStorage; 170 | -------------------------------------------------------------------------------- /dist/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | function transformStorageValue(value, version) { 4 | if (version === void 0) { version = 0; } 5 | return { 6 | _currentVersion: version, 7 | value: value 8 | }; 9 | } 10 | exports.transformStorageValue = transformStorageValue; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-use-persist-storage", 3 | "version": "1.0.2", 4 | "main": "dist/index.js", 5 | "types": "types/index.d.ts", 6 | "description": "Persist and rehydrate a context store by React Hooks", 7 | "scripts": { 8 | "test": "jest --runInBand", 9 | "build": "tsc --pretty --skipLibCheck", 10 | "prepublishOnly": "npm run build" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/visuallylab/react-native-usePersistStorage.git" 15 | }, 16 | "keywords": [ 17 | "react-native", 18 | "hooks", 19 | "async-storage", 20 | "sensitive-info", 21 | "persist-storage" 22 | ], 23 | "author": { 24 | "name": "Meng-Tse Hang & VisuallyLab", 25 | "email": "a54383813@gmail.com" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/visuallylab/react-native-usePersistStorage/issues" 29 | }, 30 | "license": "MIT", 31 | "peerDependencies": { 32 | "react": ">=16.8.1", 33 | "react-native": ">=0.59.0" 34 | }, 35 | "dependencies": { 36 | "@react-native-async-storage/async-storage": "^1.15.1", 37 | "react-native-sensitive-info": "^5.5.4" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.7.5", 41 | "@babel/preset-env": "^7.7.6", 42 | "@babel/preset-typescript": "^7.7.4", 43 | "@testing-library/react-hooks": "^3.2.1", 44 | "@types/jest": "^24.0.23", 45 | "@types/react": "^16.9.16", 46 | "@types/react-native": "^0.60.25", 47 | "@visuallylab/tslint-config-frontend": "^1.0.13", 48 | "babel-jest": "^24.9.0", 49 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 50 | "jest": "^24.9.0", 51 | "react": "^16.9.0", 52 | "react-native": "^0.61.5", 53 | "react-test-renderer": "^16.9.0", 54 | "tslint": "^5.20.1", 55 | "typescript": "^3.7.3" 56 | }, 57 | "jest": { 58 | "setupFiles": [ 59 | "./tests/setup.ts" 60 | ], 61 | "transformIgnorePatterns": [ 62 | "node_modules/(?!(@react-native-community|react-native)/)" 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/createAsyncStorage.ts: -------------------------------------------------------------------------------- 1 | import asyncStorage from '@react-native-async-storage/async-storage'; 2 | 3 | type TValue = string | null; 4 | 5 | type TCallback = (err: any, data: TValue | any[]) => void; 6 | 7 | const noop: TCallback = () => null; 8 | 9 | const createAsyncStorage = () => { 10 | return { 11 | async getItem(key: string, callback = noop) { 12 | try { 13 | const result: TValue = await asyncStorage.getItem(key); 14 | 15 | callback(null, result); 16 | 17 | return result; 18 | } catch (error) { 19 | callback(error, null); 20 | throw error; 21 | } 22 | }, 23 | 24 | async setItem(key: string, value: string, callback = noop) { 25 | try { 26 | await asyncStorage.setItem(key, value); 27 | callback(null, value); 28 | } catch (error) { 29 | callback(error, null); 30 | throw error; 31 | } 32 | }, 33 | 34 | async removeItem(key: string, callback = noop) { 35 | try { 36 | await asyncStorage.removeItem(key); 37 | callback(null, null); 38 | } catch (error) { 39 | callback(error, null); 40 | throw error; 41 | } 42 | }, 43 | } 44 | }; 45 | 46 | export default createAsyncStorage; 47 | -------------------------------------------------------------------------------- /src/createMigrate.ts: -------------------------------------------------------------------------------- 1 | import { defaultOptions } from "./defaultOptions"; 2 | import { PersistStorageValue } from "./usePersistStorage"; 3 | import { transformStorageValue } from "./utils"; 4 | 5 | export type TMigrations = { 6 | [version: number]: (state: any) => any; 7 | }; 8 | 9 | export type TMigrationFuncParams = { 10 | key: string; 11 | state: PersistStorageValue; 12 | version: number; 13 | }; 14 | 15 | export type TCreateMigrateConfig = { 16 | debug?: boolean; 17 | }; 18 | 19 | const createMigrate = ( 20 | migrations: TMigrations, 21 | configs: TCreateMigrateConfig = { debug: defaultOptions.debug } 22 | ) => { 23 | return ({ 24 | key, 25 | state, 26 | version 27 | }: TMigrationFuncParams): PersistStorageValue => { 28 | const { debug } = configs; 29 | if (!state.value) { 30 | if (debug) { 31 | console.debug(`[${key}]: no inbound value, skipping migration`); 32 | } 33 | return state; 34 | } 35 | if (state._currentVersion === version) { 36 | if (debug) { 37 | console.debug(`[${key}]: version match, no migration`); 38 | } 39 | return state; 40 | } 41 | if (state._currentVersion > version) { 42 | console.warn(`[${key}]: downgrading version is not supported`); 43 | return state; 44 | } 45 | 46 | const migrationKeys = Object.keys(migrations) 47 | .map(v => parseInt(v, 10)) 48 | .filter(ver => version >= ver && ver > state._currentVersion) 49 | .sort((a, b) => a - b); 50 | 51 | if (debug) { 52 | console.debug(`[${key}]: migration keys`, migrationKeys); 53 | } 54 | const migrated = migrationKeys.reduce(({ value }, versionKey) => { 55 | if (debug) { 56 | console.warn(`[${key}]: running migration ${versionKey}`); 57 | } 58 | return transformStorageValue(migrations[versionKey](value), versionKey); 59 | }, state); 60 | 61 | return migrated; 62 | }; 63 | }; 64 | 65 | export default createMigrate; 66 | -------------------------------------------------------------------------------- /src/createPersistContext.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import usePersistStorage, { 3 | UsePersistStorageOptions, 4 | AsyncSetState 5 | } from "./usePersistStorage"; 6 | 7 | export type PersistContext = [ 8 | T, 9 | AsyncSetState, 10 | boolean 11 | ]; 12 | 13 | const createPersistContext = ({ 14 | storageKey, 15 | defaultData, 16 | options 17 | }: { 18 | storageKey: string; 19 | defaultData: T; 20 | options?: UsePersistStorageOptions; 21 | }) => { 22 | const createDefaultData = () => defaultData; 23 | 24 | const Context = React.createContext>([ 25 | createDefaultData(), // state 26 | async () => {;}, // update state function 27 | false // restored 28 | ]); 29 | 30 | const Provider: React.FC<{ 31 | persist?: boolean; 32 | }> = props => { 33 | const [data, setData, restored] = usePersistStorage( 34 | storageKey, 35 | createDefaultData, 36 | { 37 | persist: props.persist, 38 | ...options 39 | } 40 | ); 41 | 42 | return ( 43 | 44 | {props.children} 45 | 46 | ); 47 | }; 48 | 49 | const useData = () => { 50 | const context = React.useContext(Context); 51 | if (!context) { 52 | throw new Error( 53 | `Context error: context [${storageKey}] must be used within a Provider` 54 | ); 55 | } 56 | return context; 57 | }; 58 | 59 | return { 60 | Provider, 61 | Context, 62 | useData 63 | }; 64 | }; 65 | 66 | export default createPersistContext; 67 | -------------------------------------------------------------------------------- /src/createSensitiveStorage.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native'; 2 | import sensitiveInfo, { 3 | RNSensitiveInfoOptions, 4 | } from 'react-native-sensitive-info'; 5 | 6 | type TValue = string | null; 7 | 8 | type TCallback = (err: any, data: TValue | any[]) => void; 9 | 10 | // react-native-sensitive-info returns different a different structure on iOS 11 | // than it does on Android. 12 | // 13 | // iOS: 14 | // [ 15 | // [ 16 | // { service: 'app', key: 'foo', value: 'bar' }, 17 | // { service: 'app', key: 'baz', value: 'quux' } 18 | // ] 19 | // ] 20 | // 21 | // Android: 22 | // { 23 | // foo: 'bar', 24 | // baz: 'quux' 25 | // } 26 | // 27 | // See https://github.com/mCodex/react-native-sensitive-info/issues/8 28 | // 29 | // `extractKeys` adapts for the different structure to return the list of keys. 30 | const extractKeys = Platform.select({ 31 | ios: (items: [any[]]) => items[0].map(item => item.key), 32 | android: Object.keys, 33 | }); 34 | 35 | const noop: TCallback = () => null; 36 | 37 | /** 38 | * Please use carefully, and store only secure information. 39 | * If you want to open TouchID or FaceID, please refer https://github.com/mCodex/react-native-sensitive-info#methods 40 | * @param options react-native-info options 41 | */ 42 | const createSensitiveStorage = (sensitiveOpts: RNSensitiveInfoOptions = {}) => { 43 | return { 44 | async getItem(key: string, callback = noop) { 45 | try { 46 | // getItem() returns `null` on Android and `undefined` on iOS; 47 | // explicitly return `null` here as `undefined` causes an exception 48 | // upstream. 49 | let result: TValue = await sensitiveInfo.getItem(key, sensitiveOpts); 50 | 51 | if (typeof result === 'undefined') { 52 | result = null; 53 | } 54 | 55 | callback(null, result); 56 | 57 | return result; 58 | } catch (error) { 59 | callback(error, null); 60 | throw error; 61 | } 62 | }, 63 | 64 | async setItem(key: string, value: string, callback = noop) { 65 | try { 66 | await sensitiveInfo.setItem(key, value, sensitiveOpts); 67 | callback(null, value); 68 | } catch (error) { 69 | callback(error, null); 70 | throw error; 71 | } 72 | }, 73 | 74 | async removeItem(key: string, callback = noop) { 75 | try { 76 | await sensitiveInfo.deleteItem(key, sensitiveOpts); 77 | callback(null, null); 78 | } catch (error) { 79 | callback(error, null); 80 | throw error; 81 | } 82 | }, 83 | 84 | async getAllKeys(callback = noop) { 85 | try { 86 | const values = (await sensitiveInfo.getAllItems(sensitiveOpts)) as any; 87 | const result = extractKeys(values); 88 | 89 | callback(null, result); 90 | 91 | return result; 92 | } catch (error) { 93 | callback(error, null); 94 | throw error; 95 | } 96 | }, 97 | }; 98 | }; 99 | 100 | export default createSensitiveStorage; 101 | -------------------------------------------------------------------------------- /src/defaultOptions.ts: -------------------------------------------------------------------------------- 1 | 2 | import { UsePersistStorageOptions } from './usePersistStorage'; 3 | 4 | export let defaultOptions: UsePersistStorageOptions = { 5 | debug: false, 6 | version: 0, 7 | persist: true, 8 | migrate: null, 9 | 10 | // refer from https://github.com/mCodex/react-native-sensitive-info 11 | sensitive: false, 12 | }; 13 | 14 | export type SetPersistStorageDefaultsParams = { 15 | debug?: boolean; 16 | persist?: boolean; 17 | version?: number; 18 | } 19 | 20 | export const setPersistStorageDefaults = (configs: SetPersistStorageDefaultsParams) => { 21 | defaultOptions = { ...defaultOptions, ...configs }; 22 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./usePersistStorage"; 2 | export { default as usePersistStorage } from "./usePersistStorage"; 3 | 4 | export * from "./createPersistContext"; 5 | export { default as createPersistContext } from "./createPersistContext"; 6 | 7 | export * from "./createMigrate"; 8 | export { default as createMigrate } from "./createMigrate"; 9 | 10 | export { setPersistStorageDefaults } from "./defaultOptions"; 11 | -------------------------------------------------------------------------------- /src/usePersistStorage.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef, useMemo, useCallback } from "react"; 2 | import { RNSensitiveInfoOptions } from "react-native-sensitive-info"; 3 | 4 | import { TMigrationFuncParams } from "./createMigrate"; 5 | import createAsyncStorage from "./createAsyncStorage"; 6 | import createSensitiveStorage from "./createSensitiveStorage"; 7 | import { defaultOptions } from "./defaultOptions"; 8 | import { transformStorageValue } from "./utils"; 9 | 10 | export interface PersistStorageValue { 11 | _currentVersion: number; 12 | value: Value; 13 | } 14 | 15 | export type UsePersistStorageOptions = { 16 | debug?: boolean; 17 | version?: number; 18 | persist?: boolean; 19 | migrate?: 20 | | ((params: TMigrationFuncParams) => PersistStorageValue) 21 | | null; 22 | sensitive?: false | RNSensitiveInfoOptions; 23 | }; 24 | 25 | type CallbackFn = (prev: S) => S; 26 | export type AsyncSetState = ( 27 | stateOrCallbackFn: S | CallbackFn 28 | ) => Promise; 29 | 30 | /** 31 | * usePersistStorage will return state that'll be consistent with your storage. 32 | * support migration and storing sensitive info 33 | * @param key the key stored in localStorage 34 | * @param initialValue defaultValue 35 | * @param options 36 | */ 37 | const usePersistStorage = ( 38 | key: string, 39 | initialValue: Value | (() => Value), 40 | { 41 | debug = defaultOptions.debug, 42 | version = defaultOptions.version, 43 | persist = defaultOptions.persist, 44 | migrate = defaultOptions.migrate, 45 | sensitive = defaultOptions.sensitive 46 | }: UsePersistStorageOptions = defaultOptions 47 | ): [Value, AsyncSetState, boolean] => { 48 | const currentVersion = useRef(version || 0); 49 | const [state, setState] = useState(initialValue); 50 | const [restored, setRestored] = useState(false); 51 | 52 | const Storage = useMemo( 53 | () => 54 | sensitive ? createSensitiveStorage(sensitive) : createAsyncStorage(), 55 | [sensitive] 56 | ); 57 | 58 | const logPrefix = sensitive ? "(sensitive)" : ""; 59 | 60 | const setValueToStorage = useCallback( 61 | async (newValue: Value) => { 62 | const value = transformStorageValue( 63 | newValue, 64 | currentVersion.current 65 | ); 66 | try { 67 | const serializedValue = JSON.stringify(value); 68 | await Storage.setItem(key, serializedValue); 69 | if (debug) { 70 | console.debug(`${logPrefix}[PersistStorage]: set ${key}: `, value); 71 | } 72 | } catch (err) { 73 | console.error(err); 74 | } 75 | }, 76 | [Storage] 77 | ); 78 | 79 | useEffect(() => { 80 | if (persist) { 81 | // Restore from storage when first mount. 82 | const restoreStateFromStorage = async () => { 83 | try { 84 | const storageValue = await Storage.getItem(key); 85 | if (storageValue) { 86 | let parsedValue = JSON.parse(storageValue || "null"); 87 | 88 | // format if value is incorrect 89 | if (parsedValue && parsedValue._currentVersion === undefined) { 90 | parsedValue = transformStorageValue( 91 | parsedValue, 92 | currentVersion.current 93 | ); 94 | } 95 | 96 | if (migrate) { 97 | parsedValue = migrate({ 98 | key, 99 | state: parsedValue, 100 | version: currentVersion.current 101 | }); 102 | currentVersion.current = parsedValue._currentVersion; 103 | await setValueToStorage(parsedValue.value); 104 | } 105 | 106 | setState(parsedValue.value); 107 | if (debug) { 108 | console.debug( 109 | `${logPrefix}[PersistStorage]: restore ${key}: `, 110 | parsedValue 111 | ); 112 | } 113 | } else { 114 | // If storage has no value, set initial value to storage 115 | await setValueToStorage(state); 116 | } 117 | } catch (err) { 118 | console.error(err); 119 | } 120 | 121 | setRestored(true); 122 | }; 123 | 124 | restoreStateFromStorage(); 125 | } else { 126 | // If disable persist, remove storageValue. 127 | const removePersistItem = async () => { 128 | await Storage.removeItem(key); 129 | setRestored(true); 130 | }; 131 | 132 | removePersistItem(); 133 | if (debug) { 134 | console.debug(`${logPrefix}[PersistStorage]: remove ${key}`); 135 | } 136 | } 137 | }, []); 138 | 139 | const asyncSetState: AsyncSetState = async stateOrCallbackFn => { 140 | const newValue: Value = 141 | stateOrCallbackFn instanceof Function 142 | ? stateOrCallbackFn(state) 143 | : stateOrCallbackFn; 144 | 145 | setState(newValue); 146 | if (persist) { 147 | await setValueToStorage(newValue); 148 | } 149 | }; 150 | 151 | return [state, asyncSetState, restored]; 152 | }; 153 | 154 | export default usePersistStorage; 155 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { PersistStorageValue } from "./usePersistStorage"; 2 | 3 | export function transformStorageValue( 4 | value: T, 5 | version = 0 6 | ): PersistStorageValue { 7 | return { 8 | _currentVersion: version, 9 | value 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /tests/__mocks__/@react-native-community/async-storage.js: -------------------------------------------------------------------------------- 1 | import mockStorage from '@react-native-async-storage/async-storage/jest/async-storage-mock'; 2 | export default mockStorage; 3 | -------------------------------------------------------------------------------- /tests/__mocks__/react-native-sensitive-info.js: -------------------------------------------------------------------------------- 1 | export default { 2 | setItem: jest.fn(), 3 | getItem: jest.fn(), 4 | deleteItem: jest.fn(), 5 | getAllItems: jest.fn(), 6 | }; 7 | -------------------------------------------------------------------------------- /tests/createAsyncStorage.test.ts: -------------------------------------------------------------------------------- 1 | import createAsyncStorage from "../src/createAsyncStorage"; 2 | 3 | const KEY = "@Test"; 4 | const { getItem, setItem, removeItem } = createAsyncStorage(); 5 | 6 | beforeAll(async () => { 7 | await removeItem(KEY); 8 | }) 9 | 10 | afterAll(async () => { 11 | await removeItem(KEY); 12 | }) 13 | 14 | test("test init getItem", async () => { 15 | let err; 16 | let data; 17 | const val = await getItem(KEY, (e, d) => { 18 | err = e; 19 | data = d; 20 | }); 21 | expect(err).toBeNull(); 22 | expect(data).toBeNull(); 23 | expect(val).toBeNull(); 24 | }); 25 | 26 | test("test setItem", async () => { 27 | let err; 28 | let data; 29 | await setItem(KEY, "test", (e, d) => { 30 | err = e; 31 | data = d; 32 | }); 33 | expect(err).toBeNull(); 34 | expect(data).toBe('test'); 35 | 36 | const val = await getItem(KEY); 37 | expect(val).toBe("test"); 38 | }); 39 | 40 | test("test removeItem", async () => { 41 | await removeItem(KEY); 42 | const val = await getItem(KEY); 43 | expect(val).toBeNull(); 44 | }); 45 | -------------------------------------------------------------------------------- /tests/createMigrate.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from "@testing-library/react-hooks"; 2 | import usePersistStorage from "../src/usePersistStorage"; 3 | import createAsyncStorage from "../src/createAsyncStorage"; 4 | import createMigrate from "../src/createMigrate"; 5 | import { sleep } from "./utils"; 6 | 7 | // 8 | // { 9 | // _currentVersion: number; 10 | // value: Value; 11 | // }; 12 | const transformStorageValue = (value: any, version = 0) => 13 | JSON.stringify({ 14 | _currentVersion: version, 15 | value 16 | }); 17 | 18 | const KEY = "@TEST"; 19 | const store = createAsyncStorage(); 20 | 21 | beforeAll(async () => { 22 | await store.removeItem(KEY); 23 | }); 24 | 25 | afterAll(async () => { 26 | await store.removeItem(KEY); 27 | }); 28 | 29 | test("version 0", async () => { 30 | const { result } = renderHook( 31 | props => usePersistStorage(KEY, "test", props), 32 | { 33 | initialProps: { version: 0, migrate: undefined } 34 | } 35 | ); 36 | 37 | expect(result.current[0]).toBe("test"); 38 | expect(typeof result.current[1]).toBe("function"); 39 | 40 | // wait update asyncStorage; 41 | await sleep(100); 42 | expect(await store.getItem(KEY)).toBe(transformStorageValue("test", 0)); 43 | }); 44 | 45 | test("restore & migrate version 1", async () => { 46 | const { result } = renderHook( 47 | props => usePersistStorage(KEY, "test", props), 48 | { 49 | initialProps: { 50 | version: 1, 51 | migrate: createMigrate({ 52 | 1: state => state + " migrate!" 53 | }) 54 | } 55 | } 56 | ); 57 | 58 | // wait restore and migrate asyncStorage; 59 | await sleep(300); 60 | expect(result.current[0]).toBe("test migrate!"); 61 | expect(typeof result.current[1]).toBe("function"); 62 | expect(await store.getItem(KEY)).toBe( 63 | transformStorageValue("test migrate!", 1) 64 | ); 65 | }); 66 | -------------------------------------------------------------------------------- /tests/setup.ts: -------------------------------------------------------------------------------- 1 | jest.mock('react-native', () => ({ 2 | Platform: { 3 | select: jest.fn(), 4 | } 5 | })); -------------------------------------------------------------------------------- /tests/usePersistStorage.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from "@testing-library/react-hooks"; 2 | import usePersistStorage from "../src/usePersistStorage"; 3 | import createAsyncStorage from "../src/createAsyncStorage"; 4 | import { transformStorageValue } from "../src/utils"; 5 | import { sleep } from "./utils"; 6 | 7 | const toStorageValue = (value: any, version = 0) => 8 | JSON.stringify(transformStorageValue(value, version)); 9 | 10 | const KEY = "@TEST"; 11 | const store = createAsyncStorage(); 12 | 13 | beforeAll(async () => { 14 | await store.removeItem(KEY); 15 | }); 16 | 17 | afterAll(async () => { 18 | await store.removeItem(KEY); 19 | }); 20 | 21 | test("init, setState, version = 10", async () => { 22 | const { result } = renderHook(() => 23 | usePersistStorage(KEY, "test", { version: 10 }) 24 | ); 25 | 26 | expect(result.current[0]).toBe("test"); 27 | expect(typeof result.current[1]).toBe("function"); 28 | expect(result.current[2]).toBe(false); 29 | 30 | await sleep(100); // wait mount && init asyncStorage; 31 | expect(await store.getItem(KEY)).toBe(toStorageValue("test", 10)); 32 | expect(result.current[2]).toBe(true); 33 | 34 | act(() => { 35 | result.current[1]("change"); 36 | }); 37 | expect(result.current[0]).toBe("change"); 38 | 39 | await sleep(100); // wait update asyncStorage; 40 | expect(await store.getItem(KEY)).toBe(toStorageValue("change", 10)); 41 | 42 | act(async () => { 43 | await result.current[1]("async change"); // async setState 44 | }); 45 | expect(result.current[0]).toBe("async change"); 46 | expect(await store.getItem(KEY)).toBe(toStorageValue("async change", 10)); 47 | 48 | act(async () => { 49 | await result.current[1](prev => prev + " callback"); // async setState callback 50 | }); 51 | expect(result.current[0]).toBe("async change callback"); 52 | expect(await store.getItem(KEY)).toBe(toStorageValue("async change callback", 10)); 53 | 54 | }); 55 | 56 | test("restore state", async () => { 57 | const { result } = renderHook(() => usePersistStorage(KEY, "test")); 58 | 59 | await sleep(300); // wait restore 60 | expect(result.current[0]).toBe("async change callback"); 61 | expect(typeof result.current[1]).toBe("function"); 62 | expect(result.current[2]).toBe(true); 63 | expect(await store.getItem(KEY)).toBe(toStorageValue("async change callback", 10)); 64 | }); 65 | 66 | test("no persist state", async () => { 67 | const { result } = renderHook(() => usePersistStorage(KEY, "test", { persist: false })); 68 | 69 | await sleep(300); // wait remove item 70 | expect(result.current[0]).toBe("test"); 71 | expect(typeof result.current[1]).toBe("function"); 72 | expect(await store.getItem(KEY)).toBeNull(); 73 | 74 | act(() => { 75 | result.current[1]("change"); 76 | }); 77 | 78 | expect(result.current[0]).toBe("change"); 79 | expect(await store.getItem(KEY)).toBeNull(); 80 | }); 81 | -------------------------------------------------------------------------------- /tests/utils.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (ms: number) => 2 | new Promise(resolve => setTimeout(() => resolve(), ms)); 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "declarationDir": "types", 5 | "module": "commonjs", 6 | "noImplicitAny": true, 7 | "removeComments": true, 8 | "preserveConstEnums": true, 9 | "strictNullChecks": true, 10 | "moduleResolution": "node", 11 | "esModuleInterop": true, 12 | "experimentalDecorators": true, 13 | "jsx": "react", 14 | "noUnusedParameters": true, 15 | "noUnusedLocals": true, 16 | "target": "es5", 17 | "lib": ["es6", "es2017"], 18 | "declaration": true 19 | }, 20 | "include": ["src/**/*"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@visuallylab/tslint-config-frontend"], 3 | "rules": { 4 | "no-console": [true, "log"], 5 | "interface-name": false 6 | } 7 | } -------------------------------------------------------------------------------- /types/createAsyncStorage.d.ts: -------------------------------------------------------------------------------- 1 | declare type TValue = string | null; 2 | declare type TCallback = (err: any, data: TValue | any[]) => void; 3 | declare const createAsyncStorage: () => { 4 | getItem(key: string, callback?: TCallback): Promise; 5 | setItem(key: string, value: string, callback?: TCallback): Promise; 6 | removeItem(key: string, callback?: TCallback): Promise; 7 | }; 8 | export default createAsyncStorage; 9 | -------------------------------------------------------------------------------- /types/createMigrate.d.ts: -------------------------------------------------------------------------------- 1 | import { PersistStorageValue } from "./usePersistStorage"; 2 | export declare type TMigrations = { 3 | [version: number]: (state: any) => any; 4 | }; 5 | export declare type TMigrationFuncParams = { 6 | key: string; 7 | state: PersistStorageValue; 8 | version: number; 9 | }; 10 | export declare type TCreateMigrateConfig = { 11 | debug?: boolean; 12 | }; 13 | declare const createMigrate: (migrations: TMigrations, configs?: TCreateMigrateConfig) => ({ key, state, version }: TMigrationFuncParams) => PersistStorageValue; 14 | export default createMigrate; 15 | -------------------------------------------------------------------------------- /types/createPersistContext.d.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { UsePersistStorageOptions, AsyncSetState } from "./usePersistStorage"; 3 | export declare type PersistContext = [T, AsyncSetState, boolean]; 4 | declare const createPersistContext: ({ storageKey, defaultData, options }: { 5 | storageKey: string; 6 | defaultData: T; 7 | options?: UsePersistStorageOptions | undefined; 8 | }) => { 9 | Provider: React.FC<{ 10 | persist?: boolean | undefined; 11 | }>; 12 | Context: React.Context>; 13 | useData: () => PersistContext; 14 | }; 15 | export default createPersistContext; 16 | -------------------------------------------------------------------------------- /types/createSensitiveStorage.d.ts: -------------------------------------------------------------------------------- 1 | import sensitiveInfo from 'react-native-sensitive-info'; 2 | declare type TValue = string | null; 3 | declare type TCallback = (err: any, data: TValue | any[]) => void; 4 | declare const createSensitiveStorage: (sensitiveOpts?: sensitiveInfo.RNSensitiveInfoOptions) => { 5 | getItem(key: string, callback?: TCallback): Promise; 6 | setItem(key: string, value: string, callback?: TCallback): Promise; 7 | removeItem(key: string, callback?: TCallback): Promise; 8 | getAllKeys(callback?: TCallback): Promise; 9 | }; 10 | export default createSensitiveStorage; 11 | -------------------------------------------------------------------------------- /types/defaultOptions.d.ts: -------------------------------------------------------------------------------- 1 | import { UsePersistStorageOptions } from './usePersistStorage'; 2 | export declare let defaultOptions: UsePersistStorageOptions; 3 | export declare type SetPersistStorageDefaultsParams = { 4 | debug?: boolean; 5 | persist?: boolean; 6 | version?: number; 7 | }; 8 | export declare const setPersistStorageDefaults: (configs: SetPersistStorageDefaultsParams) => void; 9 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./usePersistStorage"; 2 | export { default as usePersistStorage } from "./usePersistStorage"; 3 | export * from "./createPersistContext"; 4 | export { default as createPersistContext } from "./createPersistContext"; 5 | export * from "./createMigrate"; 6 | export { default as createMigrate } from "./createMigrate"; 7 | export { setPersistStorageDefaults } from "./defaultOptions"; 8 | -------------------------------------------------------------------------------- /types/usePersistStorage.d.ts: -------------------------------------------------------------------------------- 1 | import { RNSensitiveInfoOptions } from "react-native-sensitive-info"; 2 | import { TMigrationFuncParams } from "./createMigrate"; 3 | export interface PersistStorageValue { 4 | _currentVersion: number; 5 | value: Value; 6 | } 7 | export declare type UsePersistStorageOptions = { 8 | debug?: boolean; 9 | version?: number; 10 | persist?: boolean; 11 | migrate?: ((params: TMigrationFuncParams) => PersistStorageValue) | null; 12 | sensitive?: false | RNSensitiveInfoOptions; 13 | }; 14 | declare type CallbackFn = (prev: S) => S; 15 | export declare type AsyncSetState = (stateOrCallbackFn: S | CallbackFn) => Promise; 16 | declare const usePersistStorage: (key: string, initialValue: Value | (() => Value), { debug, version, persist, migrate, sensitive }?: UsePersistStorageOptions) => [Value, AsyncSetState, boolean]; 17 | export default usePersistStorage; 18 | -------------------------------------------------------------------------------- /types/utils.d.ts: -------------------------------------------------------------------------------- 1 | import { PersistStorageValue } from "./usePersistStorage"; 2 | export declare function transformStorageValue(value: T, version?: number): PersistStorageValue; 3 | --------------------------------------------------------------------------------