├── .gitignore ├── src ├── index.ts ├── hooks │ ├── index.ts │ ├── __mocks__ │ │ └── mockedLocalStorage.ts │ ├── useGlobalState.ts │ ├── __tests__ │ │ └── localStorage.test.ts │ └── useLocalStorage.ts ├── utils.ts └── types.ts ├── .vscode └── settings.json ├── jest.config.js ├── .github └── workflows │ ├── release.yml │ └── CreateTag.yml ├── package.json ├── CHANGELOG.md ├── LICENSE ├── tsconfig.json ├── ReadMe.md └── docs └── images ├── swr-persisted-remove.svg ├── swr-persisted-set.svg └── swr-persisted-get.svg /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # build 5 | /lib -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hooks'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "zyda", 4 | "zydalabs" 5 | ] 6 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jsdom', 4 | }; 5 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useGlobalState } from './useGlobalState'; 2 | export { default as useLocalStorage } from './useLocalStorage'; 3 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns whether or not the code is running on the server side. 3 | */ 4 | export const isServerSide: () => boolean = () => typeof window === 'undefined'; 5 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export type SetValue = (value: T) => Promise; 3 | export type RemoveValue = () => Promise; 4 | export type LocalStorageHookResult = [T|null, SetValue, RemoveValue]; 5 | 6 | export type GlobalStateHookResult = [T|null, SetValue]; 7 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v1 14 | 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 14 18 | 19 | - run: yarn install --frozen-lockfile 20 | 21 | - run: yarn build 22 | 23 | - uses: JS-DevTools/npm-publish@v1 24 | with: 25 | token: ${{ secrets.NPM_TOKEN }} 26 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/mockedLocalStorage.ts: -------------------------------------------------------------------------------- 1 | class MockedLocalStorage { 2 | public store: any; 3 | constructor() { 4 | this.store = {}; 5 | } 6 | 7 | public clear = jest.fn().mockImplementation(() => (this.store = {})); 8 | 9 | public getItem = jest.fn().mockImplementation((key) => this.store[key]); 10 | 11 | public setItem = jest.fn().mockImplementation((key, value) => { 12 | this.store[key] = value; 13 | }); 14 | 15 | public removeItem = jest.fn().mockImplementation((key) => { 16 | delete this.store[key]; 17 | }); 18 | } 19 | 20 | export default MockedLocalStorage; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zyda/swr-internal-state", 3 | "version": "1.2.0", 4 | "private": false, 5 | "description": "Use SWR to manage app's internal state", 6 | "keywords": [ 7 | "swr", 8 | "react", 9 | "internal", 10 | "global", 11 | "state" 12 | ], 13 | "repository": "git@github.com:zydalabs/swr-internal-state.git", 14 | "license": "MIT", 15 | "author": "Amr Saber ", 16 | "main": "lib/index.js", 17 | "types": "lib/index.d.ts", 18 | "files": [ 19 | "lib/**/*.*" 20 | ], 21 | "scripts": { 22 | "build": "tsc", 23 | "prepublishOnly": "yarn build", 24 | "type:check": "tsc --noEmit", 25 | "test": "jest" 26 | }, 27 | "dependencies": { 28 | "swr": "^0.4.0" 29 | }, 30 | "devDependencies": { 31 | "@types/jest": "^26.0.22", 32 | "jest": "^26.6.3", 33 | "ts-jest": "^26.5.4", 34 | "typescript": "^4.1.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.2.0] 4 | 5 | ### Added 6 | 7 | - Async `remove` for `local` storage. 8 | - Create Tags workflow on Github actions. 9 | - Async `set` for `global` and `local` storage. 10 | 11 | ### Fixed 12 | 13 | - `Set` and `Remove` functions types. 14 | 15 | ## [1.0.1] 16 | 17 | ### Fixed 18 | 19 | - Initial value in useLocaleStorageHook. 20 | 21 | ## [1.0.0] 22 | 23 | ### Using 24 | 25 | - [1.0.0-rc.4] 26 | - [1.0.0-rc.3] 27 | - [1.0.0-rc.2] 28 | - [1.0.0-rc.1] 29 | - [1.0.0-rc.0] 30 | 31 | ## [1.0.0-rc.4] 32 | 33 | ### Added 34 | 35 | - `CI`: add auto release action 36 | 37 | ## [1.0.0-rc.3] 38 | 39 | ### Added 40 | 41 | - Update readme to include installation guide.CI: add auto release action 42 | 43 | ## [1.0.0-rc.2] 44 | 45 | ### Added 46 | 47 | - Test Cases. 48 | 49 | ## [1.0.0-rc.1] 50 | 51 | ### Added 52 | 53 | - Docs. 54 | 55 | ## [1.0.0-rc.0] 56 | 57 | - Create a `swr internal state` package to manage app's internal state using `SWR`. 58 | -------------------------------------------------------------------------------- /src/hooks/useGlobalState.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | 3 | import { GlobalStateHookResult } from '../types'; 4 | 5 | /** 6 | * Gets and sets value to/from global state. 7 | * 8 | * All states with the same key are shared with each other. 9 | * 10 | * @param key key to get and set value to/from local storage. 11 | * @param defaultValue default value that is returned incase the key was not found. 12 | * 13 | * @returns an array of (the saved value, set value function) in the same order. 14 | */ 15 | const useGlobalState = (key: string, defaultValue: T|null = null): GlobalStateHookResult => { 16 | const { data: state = defaultValue, mutate } = useSWR( 17 | key, 18 | null, 19 | { 20 | revalidateOnFocus: false, 21 | revalidateOnReconnect: false, 22 | refreshWhenHidden: false, 23 | refreshWhenOffline: false, 24 | } 25 | ); 26 | 27 | const setState = async (value: T): Promise => { await mutate(value, false); }; 28 | 29 | return [state, setState]; 30 | }; 31 | 32 | export default useGlobalState; 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Zyda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "es2015", 6 | "dom" 7 | ], 8 | "outDir": "lib", 9 | "jsx": "react", 10 | "module": "commonjs", 11 | "declaration": true, 12 | "declarationMap": true, 13 | "sourceMap": true, 14 | "allowJs": true, 15 | "skipLibCheck": true, 16 | "strict": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "noImplicitAny": false, 19 | "strictNullChecks": true, 20 | "strictFunctionTypes": true, 21 | "strictPropertyInitialization": true, 22 | "noImplicitThis": true, 23 | "alwaysStrict": true, 24 | "noUnusedLocals": true, 25 | "noUnusedParameters": true, 26 | "noImplicitReturns": true, 27 | "noFallthroughCasesInSwitch": true, 28 | "moduleResolution": "node", 29 | "resolveJsonModule": true, 30 | "isolatedModules": true, 31 | "esModuleInterop": true, 32 | "allowSyntheticDefaultImports": true 33 | }, 34 | "include": [ 35 | "src" 36 | ], 37 | "exclude": [ 38 | "node_modules", 39 | "src/hooks/__mocks__", 40 | "src/hooks/__tests__" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/CreateTag.yml: -------------------------------------------------------------------------------- 1 | name: Create Tag 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | CI: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | ref: ${{ github.event.pull_request.head.sha }} 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: '14.x' 18 | registry-url: 'https://npm.pkg.github.com' 19 | scope: '@zydalabs' 20 | 21 | - name: Get Changelog Entry 22 | id: changelog_reader 23 | uses: mindsers/changelog-reader-action@v2 24 | with: 25 | validation_depth: 5 26 | path: ./CHANGELOG.md 27 | 28 | - name: Tag Deployment 29 | uses: tvdias/github-tagger@v0.0.1 30 | with: 31 | repo-token: ${{ secrets.GITHUB_TOKEN }} 32 | tag: ${{ steps.changelog_reader.outputs.version }} 33 | 34 | - name: Create/update release 35 | uses: ncipollo/release-action@v1 36 | with: 37 | tag: ${{ steps.changelog_reader.outputs.version }} 38 | name: Release ${{ steps.changelog_reader.outputs.version }} 39 | body: ${{ steps.changelog_reader.outputs.changes }} 40 | prerelease: ${{ steps.changelog_reader.outputs.status == 'prereleased' }} 41 | draft: ${{ steps.changelog_reader.outputs.status == 'unreleased' }} 42 | allowUpdates: true 43 | token: ${{ secrets.GITHUB_TOKEN }} 44 | -------------------------------------------------------------------------------- /src/hooks/__tests__/localStorage.test.ts: -------------------------------------------------------------------------------- 1 | import useLocalStorage from "../useLocalStorage"; 2 | import MockedLocalStorage from "../__mocks__/mockedLocalStorage"; 3 | 4 | let mockedMutate = jest.fn(); 5 | 6 | jest.mock("swr", () => (key, persistentFetcher) => ({ data: persistentFetcher(key), mutate: mockedMutate })); 7 | 8 | describe("persistent-storage", () => { 9 | let mockedLocalStorage: MockedLocalStorage = new MockedLocalStorage(); 10 | 11 | beforeAll(() => { 12 | Object.defineProperty(window, "localStorage", { value: mockedLocalStorage }); 13 | }); 14 | 15 | beforeEach(() => { 16 | mockedLocalStorage.clear(); 17 | }); 18 | 19 | it("should set item in local-storage and call mutate in swr", () => { 20 | const [, setPersistentKey] = useLocalStorage("key"); 21 | setPersistentKey("value"); 22 | expect(mockedLocalStorage.setItem).toBeCalled(); 23 | expect(mockedMutate).toBeCalled(); 24 | expect(mockedLocalStorage.store["key"]).toEqual(JSON.stringify("value")); 25 | }); 26 | 27 | it("should get item from local-storage", () => { 28 | mockedLocalStorage.store["key"] = JSON.stringify("value"); 29 | const [persistentValue] = useLocalStorage("key"); 30 | expect(mockedLocalStorage.getItem).toBeCalled(); 31 | expect(persistentValue).toEqual("value"); 32 | }); 33 | 34 | it("should remove item from local-storage", () => { 35 | mockedLocalStorage.store["key"] = JSON.stringify("value"); 36 | const [, , removePersistentKeyValue] = useLocalStorage("key"); 37 | removePersistentKeyValue(); 38 | expect(mockedLocalStorage.removeItem).toBeCalled(); 39 | expect(mockedMutate).toBeCalled(); 40 | expect(mockedLocalStorage.store["key"]).toEqual(undefined); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/hooks/useLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | 3 | import { LocalStorageHookResult } from '../types'; 4 | import { isServerSide } from '../utils'; 5 | 6 | 7 | /** 8 | * Gets and sets value to/from local storage. 9 | * 10 | * @param key key to get and set value to/from local storage. 11 | * @param defaultValue default value that is returned incase the key was not found. 12 | * 13 | * @returns an array of (the saved value, set value function, and remove value function) in the same order. 14 | */ 15 | const useLocalStorage = (key: string, defaultValue: T|null = null): LocalStorageHookResult => { 16 | let initialValue = defaultValue; 17 | 18 | if(!isServerSide()) { 19 | let storedValue = window.localStorage.getItem(key); 20 | if(storedValue !== null && storedValue !== 'undefined') 21 | initialValue = JSON.parse(storedValue); 22 | } 23 | 24 | const { data: value = initialValue, mutate } = useSWR( 25 | key, 26 | null, 27 | { 28 | revalidateOnFocus: false, 29 | revalidateOnReconnect: false, 30 | refreshWhenHidden: false, 31 | refreshWhenOffline: false, 32 | } 33 | ); 34 | 35 | // ========== Set value ========== 36 | const setValue = async (value: T): Promise => { 37 | await mutate(value, false); 38 | 39 | if (isServerSide()) { return; } 40 | 41 | // Save to local storage 42 | const localStorageValue = JSON.stringify(value); 43 | window.localStorage.setItem(key, localStorageValue); 44 | }; 45 | 46 | // ========== Remove value ========== 47 | const removeValue = async (): Promise => { 48 | await mutate(defaultValue, false); 49 | 50 | if (isServerSide()) { return; } 51 | 52 | // Remove value from local storage 53 | window.localStorage.removeItem(key); 54 | }; 55 | 56 | return [value, setValue, removeValue]; 57 | }; 58 | 59 | export default useLocalStorage; 60 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # SWR Internal State 2 | This package provides 2 hooks that can be used to manage react (or Next.js) project's internal state in ways: in memory state, and local storage persistent state. 3 | 4 | This package is based on [swr](https://swr.vercel.app/) as they have built a very good caching method that can be generalized and used with different use cases other than fetching and caching data from an external service. Like managing internal app state and syncing that state between different components. 5 | 6 | The 2 hooks that we expose are: 7 | - `useGlobalState`: used for managing and syncing in-memory state. 8 | - `useLocalStorage`: used for managing and syncing local storage persistent state. 9 | 10 | ## When to Use 11 | This package was made with the intention to be used to manage the internal states of projects that rely heavily on external services and has simple internal states. So -for small/simple states- it can be a convenient replacement for `Redux`, `PullState`, and even React's `Context` API. 12 | 13 | ## Installation 14 | With npm... 15 | ```bash 16 | npm i @zyda/swr-internal-state 17 | ``` 18 | 19 | Or with yarn... 20 | ```bash 21 | yarn add @zyda/swr-internal-state 22 | ``` 23 | 24 | ## How it works 25 | The 2 used hooks accept similar parameters and have similar return types. Each of them accept a key and a default value. And both of them return an array of the state and its management functions (the exact interface of the hooks is explained below). 26 | 27 | These 2 hooks are intended to be used as a building block for other custom hooks that are used to encapsulate the whole state. 28 | I.e. If you want to save a state that contains user info, you should create a custom hook `useUserInfo` that looks like the following... 29 | ```tsx 30 | const useUserInfo= () => useGlobalState('user-info', { name: '', age: null }); 31 | 32 | // And in your components... 33 | const UserName = () => { 34 | const [userInfo, setUserInfo] = useUserInfo(); 35 | return {userInfo.name}; 36 | } 37 | 38 | const UpdateUser = () => { 39 | const [userInfo, setUserInfo] = useUserInfo(); 40 | 41 | const updateUser = () => { 42 | const userName = /* some logic */; 43 | const userAge = /* some logic */; 44 | setUserInfo({ name: userName, age: userAge }); 45 | } 46 | 47 | // Component logic... 48 | 49 | return ( 50 |
51 | {/* Some UI */} 52 | 53 |
54 | ); 55 | } 56 | 57 | ``` 58 | When you use `setUserInfo` it updates `userInfo` state in all the components that use it as if it was raised to their parent and shared from it (but that's not how SWR manage their state). 59 | 60 | ## Hooks Interface 61 | 62 | ### In-memory Hook 63 | ```ts 64 | useGlobalState(key: string, defaultValue: T): [T, setValue: (T) => void] 65 | ``` 66 | 67 | This hook is used to manage and sync in-memory state between different components. The saved state is not persisted and will be reset with page refresh. 68 | 69 | Parameters: 70 | - `key` (required): a unique key to the state that you want to manage. Used to differentiate states from each other (for example: to tell the difference between `user-info` and `user-cart` states) 71 | - `defaultValue` (optional, defaults to `null`): The initial value of that state. 72 | 73 | Return values: 74 | The hook returns an array that contains 2 values: 75 | 1. The state: what ever you decide to save into that state. Initially it's value equals `defaultValue`. 76 | 1. `setState` function: A function that accepts 1 parameter: the new state. 77 | 78 | ### Local Storage Persistent State 79 | ```ts 80 | useLocalStorage(key: string, defaultValue: T): [T, setValue: (T) => void, removeValue: () => void] 81 | ``` 82 | 83 | This hook is used to manage and sync persisted state between different components. The saved state will be saved to the local storage and it will survive refreshing the page and closing the browser. 84 | 85 | Parameters: 86 | - `key` (required): a unique key to the state that you want to manage. Used to differentiate states from each other (for example: to tell the difference between `user-info` and `user-cart` states) 87 | - `defaultValue` (optional, defaults to `value stored in locale storage if exisits or null`): The initial value of that state. 88 | 89 | Return values: 90 | 1. The state: what ever you decide to save into that state. Initially it's value equals `defaultValue`. 91 | 1. `setState` function: A function that accepts 1 parameter: the new state. 92 | 1. `removeValue` function: A function that does not accept any value. Used to clear the local storage from the saved state and sets the state to `defaultValue`. 93 | -------------------------------------------------------------------------------- /docs/images/swr-persisted-remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | ClientuseLocalStorageSWRLocal Storageremoveremovemutateremovesyncre-renderRemove ValueMade with Excalidraw -------------------------------------------------------------------------------- /docs/images/swr-persisted-set.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | ClientuseLocalStorageSWRLocal StorageSavesetmutatesetsyncre-renderStore ValueMade with Excalidraw -------------------------------------------------------------------------------- /docs/images/swr-persisted-get.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | ClientuseLocalStorageSWRLocal StorageCacheValuegetusegetvaluevalue, setValue, removeValueGetting Stored ValueMade with Excalidraw --------------------------------------------------------------------------------