├── .gitignore ├── .storybook ├── addons.js ├── config.js ├── tsconfig.json └── webpack.config.js ├── README.md ├── package-lock.json ├── package.json ├── src ├── index.stories.tsx └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | // automatically import all files ending in *.stories.js 4 | const req = require.context('../src', true, /.stories.(js|ts|tsx)$/); 5 | function loadStories() { 6 | req.keys().forEach(filename => req(filename)); 7 | } 8 | 9 | configure(loadStories, module); 10 | -------------------------------------------------------------------------------- /.storybook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": [ 4 | "../src" 5 | ], 6 | "exclude": [ 7 | "../lib", 8 | "../node_modules" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (baseConfig, env, config) => { 2 | config.module.rules.push({ 3 | test: /\.(ts|tsx)$/, 4 | use: [{ 5 | loader: require.resolve('awesome-typescript-loader'), 6 | options: { 7 | configFileName: './.storybook/tsconfig.json' 8 | } 9 | }] 10 | }); 11 | config.resolve.extensions.push('.ts', '.tsx'); 12 | return config; 13 | }; 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-use-local-storage 2 | 3 | React hook that persists state with the [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) API. It also automatically syncs state between tabs/windows. 4 | 5 | ## Getting started 6 | 7 | ``` 8 | npm i @illinois/react-use-local-storage 9 | ``` 10 | 11 | This hook functions similarly to `useState`, with the exception of the the caveats listed below. If your state is easily encodable in JSON and you don't use lazy initialization or functional updates, it should be easy to migrate from `useState`. You'll need to provide a key for getting/setting the item in local storage. 12 | 13 | ```jsx 14 | import useLocalStorage from '@illinois/react-use-local-storage' 15 | 16 | const MyComponent = () => { 17 | const [count, setCount] = useLocalStorage('value-key', 0) 18 | return ( 19 | <> 20 |
Count: {count}
21 | 22 | 23 | 24 | ) 25 | } 26 | ``` 27 | 28 | ## Trying it out 29 | 30 | ```sh 31 | git clone https://github.com/illinois/react-use-local-storage.git react-use-local-storage 32 | cd react-use-local-storage 33 | npm install 34 | npm run storybook 35 | ``` 36 | 37 | This will launch a simple demo with a counter that can be incremented and reset. Try opening the demo in two tabs at once and watch how changes are automatically synced between them! 38 | 39 | ## Caveats/Warnings 40 | 41 | * State is serialized with `JSON.stringify` and deserialized with `JSON.parse`. This is done because the `localStorage` API doesn't support storing anything but strings at present. As such, you should pay special attention to objects that might not handle being round-tripped through JSON, e.g. a `Date` object. 42 | 43 | * Unlike `useState`, [lazy initialization](https://reactjs.org/docs/hooks-reference.html#lazy-initial-state) is not currently supported. A PR adding support would be welcome! 44 | 45 | * Unlike `useState`, [functional updates](https://reactjs.org/docs/hooks-reference.html#functional-updates) are not currently supported. A PR adding support would be welcome! 46 | 47 | ## Prior art 48 | 49 | * https://github.com/streamich/react-use/blob/master/docs/useLocalStorage.md 50 | * https://github.com/rehooks/local-storage/blob/master/index.js 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@illinois/react-use-local-storage", 3 | "version": "1.1.0", 4 | "description": "React hook that persists and syncs state with local storage", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib/" 8 | ], 9 | "scripts": { 10 | "build": "tsc", 11 | "storybook": "start-storybook -p 6006", 12 | "build-storybook": "build-storybook" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/illinois/react-use-local-storage.git" 17 | }, 18 | "keywords": [ 19 | "react", 20 | "hooks", 21 | "react-hooks" 22 | ], 23 | "author": "Nathan Walters (@nwalters512)", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/illinois/react-use-local-storage/issues" 27 | }, 28 | "homepage": "https://github.com/illinois/react-use-local-storage#readme", 29 | "devDependencies": { 30 | "@babel/core": "^7.2.2", 31 | "@storybook/addon-actions": "^4.1.11", 32 | "@storybook/addon-links": "^4.1.11", 33 | "@storybook/addons": "^4.1.11", 34 | "@storybook/react": "^4.1.11", 35 | "@types/react": "^16.8.3", 36 | "@types/storybook__react": "^4.0.1", 37 | "awesome-typescript-loader": "^5.2.1", 38 | "babel-loader": "^8.0.5", 39 | "react": "^16.8.2", 40 | "typescript": "^3.3.3" 41 | }, 42 | "peerDependencies": { 43 | "react": "^16.8.0", 44 | "react-dom": "^16.8.0" 45 | }, 46 | "dependencies": {} 47 | } 48 | -------------------------------------------------------------------------------- /src/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { storiesOf } from '@storybook/react'; 4 | 5 | import useLocalStorage from '../src/index' 6 | 7 | const UseLocalStorageDemo = () => { 8 | const [count, setCount] = useLocalStorage('demo-count', 0) 9 | return ( 10 | <> 11 |
Count: {count}
12 | 13 | 14 | 15 | ) 16 | } 17 | 18 | storiesOf('useLocalStorage', module) 19 | .add('demo', () => ) 20 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'react' 2 | 3 | const isClient = typeof window !== 'undefined' 4 | 5 | const useLocalStorage = (key: string, initialValue: T): [T, (value: T) => void] => { 6 | if (!isClient) { 7 | // We're SSRing; can't use local storage here! 8 | return [initialValue, () => {}] 9 | } 10 | const [state, updateState] = useState((): T => { 11 | try { 12 | const localStorageValue = window.localStorage.getItem(key) 13 | if (localStorageValue === null) { 14 | // Initialize local storage with default state 15 | window.localStorage.setItem(key, JSON.stringify(initialValue)) 16 | return initialValue 17 | } else { 18 | return JSON.parse(localStorageValue) 19 | } 20 | } catch { 21 | // User might be facing storage restrictions, or JSON 22 | // serialization/deserialization may have failed. We can just fall back 23 | // to using React state here. 24 | return initialValue 25 | } 26 | }) 27 | const localStorageChanged = (e: StorageEvent) => { 28 | if (e.key === key) { 29 | updateState(JSON.parse(e.newValue as string)) 30 | } 31 | } 32 | const setState = useCallback((value: T) => { 33 | window.localStorage.setItem(key, JSON.stringify(value)) 34 | updateState(value) 35 | }, [key, updateState]) 36 | useEffect(() => { 37 | window.addEventListener('storage', localStorageChanged) 38 | return () => { 39 | window.removeEventListener('storage', localStorageChanged) 40 | } 41 | }) 42 | return [state, setState] 43 | } 44 | 45 | export default useLocalStorage 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "jsx": "react", 7 | "declaration": true, 8 | "pretty": true, 9 | "rootDir": "src", 10 | "sourceMap": true, 11 | "strict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noImplicitAny": false, 16 | "noFallthroughCasesInSwitch": true, 17 | "outDir": "lib", 18 | "lib": ["es2018", "dom"] 19 | }, 20 | "include": [ 21 | "src" 22 | ], 23 | "exclude": [ 24 | "node_modules", 25 | "lib", 26 | "**/*.stories.tsx" 27 | ] 28 | } 29 | --------------------------------------------------------------------------------