├── assets └── gudam.jpg ├── .vscode └── settings.json ├── .npmignore ├── tsconfig.node.json ├── .prettierrc ├── tsconfig.json ├── .eslintrc.json ├── LICENSE ├── package.json ├── .gitignore ├── src └── index.ts └── README.md /assets/gudam.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasinoorit/gudam/HEAD/assets/gudam.jpg -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Gudam", 4 | "proxyfy", 5 | "Rinia" 6 | ] 7 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | !dist/ 2 | src 3 | .vscode 4 | .prettierrc 5 | .eslintrc.json 6 | tsconfig.json 7 | tsconfig.node.json 8 | assets -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | } 9 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 88, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": true, 10 | "arrowParens": "always" 11 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/*" 4 | ], 5 | "compilerOptions": { 6 | "baseUrl": ".", 7 | "rootDir": ".", 8 | "outDir": "dist", 9 | "sourceMap": false, 10 | "noEmit": true, 11 | "target": "esnext", 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "allowJs": false, 15 | "skipLibCheck": true, 16 | "noUnusedLocals": true, 17 | "strictNullChecks": true, 18 | "noImplicitAny": true, 19 | "noImplicitThis": true, 20 | "noImplicitReturns": false, 21 | "strict": true, 22 | "isolatedModules": true, 23 | "experimentalDecorators": true, 24 | "resolveJsonModule": true, 25 | "esModuleInterop": true, 26 | "removeComments": false, 27 | "jsx": "preserve", 28 | "lib": [ 29 | "esnext", 30 | "dom" 31 | ], 32 | } 33 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:react/recommended", 11 | "prettier" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaVersion": "latest", 16 | "sourceType": "module" 17 | }, 18 | "plugins": [ 19 | "@typescript-eslint", 20 | "react" 21 | ], 22 | "rules": { 23 | "indent": [ 24 | "error", 25 | 2 26 | ], 27 | "linebreak-style": [ 28 | "error", 29 | "unix" 30 | ], 31 | "quotes": [ 32 | "error", 33 | "single" 34 | ], 35 | "semi": [ 36 | "error", 37 | "never" 38 | ] 39 | } 40 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 MD. HASHINUR RAHMAN 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gudam", 3 | "version": "1.0.0", 4 | "description": "Gudam is a tiny store management library for react.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/hasinoorit/gudam.git" 8 | }, 9 | "keywords": [ 10 | "react", 11 | "pinia", 12 | "store", 13 | "redux", 14 | "state" 15 | ], 16 | "scripts": { 17 | "build": "npx unbuild" 18 | }, 19 | "author": "Md. Hashinur Rahman", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/hasinoorit/gudam/issues" 23 | }, 24 | "homepage": "https://github.com/hasinoorit/gudam#readme", 25 | "unbuild": { 26 | "declaration": true, 27 | "rollup": { 28 | "emitCJS": true, 29 | "esbuild": { 30 | "minify": true 31 | } 32 | } 33 | }, 34 | "peerDependencies": { 35 | "react": "^18.2.0" 36 | }, 37 | "type": "module", 38 | "exports": { 39 | ".": { 40 | "import": "./dist/index.mjs", 41 | "require": "./dist/index.cjs", 42 | "types": "./dist/index.d.ts" 43 | } 44 | }, 45 | "main": "./dist/index.mjs", 46 | "types": "./dist/index.d.ts", 47 | "files": [ 48 | "dist" 49 | ], 50 | "devDependencies": { 51 | "@types/react": "^18.2.36", 52 | "@typescript-eslint/eslint-plugin": "^6.11.0", 53 | "@typescript-eslint/parser": "^6.11.0", 54 | "eslint": "^8.53.0", 55 | "eslint-config-prettier": "^9.0.0", 56 | "eslint-plugin-react": "^7.33.2", 57 | "prettier": "3.1.0", 58 | "typescript": "^5.2.2" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | yarn.lock 132 | package-lock.json -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { useState, createContext, useContext } from 'react' 2 | export const GudamContext = createContext>({}) 3 | type IfEquals = (() => T extends X ? 1 : 2) extends < 4 | T 5 | >() => T extends Y ? 1 : 2 6 | ? A 7 | : B 8 | type WritableKeysOf = { 9 | [P in keyof T]: IfEquals< 10 | { [Q in P]: T[P] }, 11 | { -readonly [Q in P]: T[P] }, 12 | P, 13 | never 14 | > 15 | }[keyof T] 16 | type WritablePart = Pick> 17 | type FunctionPropertyNames = { 18 | // eslint-disable-next-line @typescript-eslint/ban-types 19 | [K in keyof T]: T[K] extends Function ? K : never 20 | }[keyof T] 21 | 22 | type OnlyState = Omit< 23 | WritablePart, 24 | FunctionPropertyNames> 25 | > 26 | 27 | type PreloadCB = (initialState: OnlyState) => OnlyState 28 | type $Preload = { 29 | $preload: (cb: PreloadCB) => void 30 | } 31 | 32 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 33 | type PersistOptions = { 34 | storage?: Storage 35 | version?: string 36 | stringify?: (value: T) => string 37 | parse?: (str: string) => T 38 | } 39 | const clone = (store: T) => 40 | new Proxy(store, { 41 | set() { 42 | return true 43 | }, 44 | }) 45 | 46 | const createGudam = () => { 47 | const stores: Record = {} 48 | const persistRecords: Record = {} 49 | const defineStore = ( 50 | storeKey: string, 51 | store: T, 52 | persistOptions: PersistOptions> = {} 53 | ) => { 54 | stores[storeKey] = store 55 | persistRecords[storeKey] = persistOptions 56 | const useStore = (ctx = useContext(GudamContext)): T & $Preload => { 57 | return ctx[storeKey] as T & $Preload 58 | } 59 | return useStore 60 | } 61 | const useGudam = () => { 62 | const [state, setState] = useState(() => { 63 | const storesInstance: Record = {} 64 | for (const key in stores) { 65 | if (Object.prototype.hasOwnProperty.call(stores, key)) { 66 | let hasChanged = false 67 | const descriptions = Object.getOwnPropertyDescriptors(stores[key]) 68 | let states: Record = {} 69 | let hasPending = false 70 | const dataKey = `gudam_data__${key}` 71 | const versionKey = `gudam_version__${key}` 72 | const { 73 | storage, 74 | parse = JSON.parse, 75 | stringify = JSON.stringify, 76 | version = '0.0.1', 77 | } = persistRecords[key] 78 | const persist = storage 79 | ? () => { 80 | if (!hasPending) { 81 | hasPending = true 82 | setTimeout(() => { 83 | hasPending = false 84 | storage.setItem(dataKey, stringify(states)) 85 | }, 0) 86 | } 87 | } 88 | : () => { } 89 | const store = { 90 | $preload: (cb: PreloadCB) => { 91 | if (!hasChanged) { 92 | states = cb(states) 93 | hasChanged = true 94 | persist() 95 | } 96 | }, 97 | } 98 | for (const key in descriptions) { 99 | if (Object.prototype.hasOwnProperty.call(descriptions, key)) { 100 | const item = descriptions[key] 101 | if (item.writable && typeof item.value !== 'function') { 102 | states[key] = item.value 103 | Object.defineProperty(store, key, { 104 | get() { 105 | return states[key] 106 | }, 107 | set(v) { 108 | states[key] = v 109 | hasChanged = true 110 | setState((oldState: object) => ({ 111 | ...oldState, 112 | [key]: clone(store), 113 | })) 114 | persist() 115 | }, 116 | }) 117 | } else if (typeof item.value === 'function') { 118 | Object.defineProperty(store, key, { 119 | ...item, 120 | value: item.value.bind(store), 121 | }) 122 | } else { 123 | Object.defineProperty(store, key, item) 124 | } 125 | } 126 | } 127 | if (storage) { 128 | const oldVersion = storage.getItem(versionKey) 129 | if (oldVersion !== version) { 130 | storage.setItem(dataKey, stringify(states)) 131 | storage.setItem(versionKey, version) 132 | } else { 133 | const oldData = storage.getItem(dataKey) 134 | if (oldData) { 135 | states = parse(oldData) 136 | } 137 | } 138 | } 139 | storesInstance[key] = clone(store) 140 | } 141 | } 142 | return storesInstance 143 | }) 144 | return state 145 | } 146 | return { defineStore, useGudam } 147 | } 148 | 149 | export const { defineStore, useGudam } = createGudam() 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gudam (গুদাম): Simplifying React State Management 2 | 3 | ![Gudam Banner](/assets/gudam.jpg) 4 | 5 | ## Introduction 6 | 7 | **Gudam** stands as a robust yet lightweight React state management library, designed to elevate your application's performance. With an impressively small footprint of just 1kb, it boasts quick execution, straightforward implementation, and scalability. This TypeScript-based library ensures reliable and statically-typed state management for your React applications. 8 | 9 | ## Installation 10 | 11 | Getting started with Gudam is a breeze. Simply install it using npm with the following command: 12 | 13 | ```bash 14 | npm install gudam 15 | ``` 16 | 17 | ## Setting Up Gudam 18 | 19 | Integrating Gudam into your root component is a straightforward process. Follow these steps: 20 | 21 | ```javascript 22 | import { GudamContext, useGudam } from 'gudam' 23 | 24 | function App() { 25 | const gudam = useGudam() 26 | return ( 27 | 28 | {/* Your application is now powered by Gudam */} 29 |
Your App Content
30 |
31 | ) 32 | } 33 | 34 | export default App 35 | ``` 36 | 37 | ## Creating a Store 38 | 39 | Define a store in Gudam using the `defineStore` function. The following example illustrates how to define a store for authentication, including additional details: 40 | 41 | ```javascript 42 | import { defineStore } from 'gudam' 43 | 44 | export const authGudam = defineStore( 45 | 'auth', // Unique key 46 | { 47 | // states 48 | token: '', // Authentication token 49 | role: '', // User role 50 | profile: { 51 | firstName: '', // User's first name 52 | lastName: '', // User's last name 53 | }, 54 | // accessors 55 | get isLoggedIn() { 56 | return !!this.token && !!this.role; // Check if the user is logged in 57 | }, 58 | get greetingMessage() { 59 | return this.profile.firstName 60 | ? `Welcome back ${this.profile.firstName}` 61 | : 'Welcome Guest'; 62 | }, 63 | // methods 64 | setAuth(token, role) { 65 | this.token = token; 66 | this.role = role; 67 | }, 68 | setFirstName(name: string) { 69 | this.profile = { ...this.profile, firstName: name }; 70 | }, 71 | }, 72 | { 73 | version: '0.0.1', // Version of the store 74 | storage: localStorage, // Storage engine (localStorage or sessionStorage) 75 | parse: JSON.parse, // Parser function to parse from string to store state 76 | stringify: JSON.stringify, // Converts stateful properties of your store to JSON string 77 | } 78 | ); 79 | ``` 80 | 81 | `defineStore` accepts three parameters: 82 | 83 | 1. **`key`**: A unique identifier for your store. 84 | 2. **`store`**: Your store object with states, accessors, and methods. 85 | 86 | - `states`: Non-function-type writable properties are states. In our `authGudam` store, we have `token`, `role`, and `profile` states. 87 | - `accessors`: Accessors are functions with the `get` identifier, providing a way to access computed or derived properties from the state. 88 | - `methods`: Methods are pure JavaScript functions. You can use these functions to make changes to your store states. 89 | 90 | > The store object is a regular JavaScript object, except that it triggers effects if changes are made to any state property. 91 | 92 | 3. **`persistOptions`** (optional): An object with persist options. 93 | 94 | | Property | Description | Default Value | 95 | | --------- | ----------------------------------------------------------------------------------------------------- | -------------- | 96 | | storage\* | Storage engine (`localStorage` or `sessionStorage`). It is required to enable persistence | undefined | 97 | | version | Version of your current store. Change the version to invalidate old store values in the browser cache | 0.0.1 | 98 | | parse | Parser function to convert from string to store state | JSON.parse | 99 | | stringify | Converts stateful properties of your store to a JSON string | JSON.stringify | 100 | 101 | ## Accessing the Store 102 | 103 | Access and utilize the store in your components as follows: 104 | 105 | ```javascript 106 | import { gudamAuth } from '../store/authStore' 107 | 108 | const Greeting = () => { 109 | const auth = gudamAuth() 110 | const loginAsDemo = () => { 111 | auth.setFirstName('Demo') 112 | } 113 | 114 | return ( 115 | <> 116 |
{auth.greetingMessage}
117 | {!auth.username && ( 118 |
119 | 120 |
121 | )} 122 | 123 | ) 124 | } 125 | export default Greeting 126 | ``` 127 | 128 | ## Preloading Data 129 | 130 | You can use the `$preload` built-in function to preload data into your store. It takes a callback function that receives an object with initial state values and returns the updated state. The following example provides additional details: 131 | 132 | ```javascript 133 | import { GudamContext, useGudam } from 'gudam' 134 | import { gudamAuth } from '../store/authStore' 135 | 136 | function App() { 137 | const gudam = useGudam() 138 | const auth = gudamAuth(gudam) // providing the gudam instance because the context is not provided yet 139 | auth.$preload((states) => { 140 | return { ...states, token: 'jwt-token', role: 'user' } 141 | }) 142 | return ( 143 | 144 | {/* Your application is now empowered by Gudam */} 145 |
Your App Content
146 |
147 | ) 148 | } 149 | 150 | export default App 151 | ``` 152 | 153 | Please note that once the store is changed, `$preload` does not execute its callback, and it does not support asynchronous operations. 154 | 155 | ## Store Inside Store 156 | 157 | Gudam follows a modular design, allowing you to use one store inside another. This can be achieved by creating an instance of the store: 158 | 159 | ```javascript 160 | export const gudamAccount = defineStore('account', { 161 | balance: 0, 162 | get auth() { 163 | return gudamAuth() // Create an instance of gudamAuth 164 | }, 165 | doSomething() { 166 | this.auth.setFirstName('Hasinoor') 167 | }, 168 | }) 169 | ``` 170 | 171 | ## Need Help? 172 | 173 | If you require further assistance or have any questions, please don't hesitate to create an issue. The Gudam team is committed to simplifying your state management experience! 174 | --------------------------------------------------------------------------------