├── src ├── app │ ├── src │ │ ├── setupTests.ts │ │ ├── react-app-env.d.ts │ │ ├── index.tsx │ │ ├── reportWebVitals.ts │ │ ├── App.test.tsx │ │ └── App.tsx │ ├── public │ │ ├── robots.txt │ │ ├── favicon.ico │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── index.html │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ └── README.md ├── app-ssr │ ├── src │ │ └── app │ │ │ ├── store.ts │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ ├── next.config.ts │ ├── eslint.config.mjs │ ├── package.json │ ├── .gitignore │ └── tsconfig.json └── package │ ├── index.ts │ ├── signify-cache │ ├── cache.model.ts │ └── index.ts │ ├── signify-core │ ├── signify.model.ts │ ├── signify.core.ts │ └── index.ts │ ├── utils │ ├── cookies.ts │ ├── objectCompare.ts │ └── objectClone.ts │ ├── signify-sync │ └── index.ts │ └── signify-devTool │ ├── index.css │ ├── index.scss │ └── index.tsx ├── .gitignore ├── .prettierrc ├── tsconfig.json ├── LICENSE ├── package.json ├── CODE_OF_CONDUCT.md ├── rollup.config.js └── README.md /src/app/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | 4 | package-lock.json 5 | yarn.lock -------------------------------------------------------------------------------- /src/app/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VietCPQ94/react-signify/HEAD/src/app/public/favicon.ico -------------------------------------------------------------------------------- /src/app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VietCPQ94/react-signify/HEAD/src/app/public/logo192.png -------------------------------------------------------------------------------- /src/app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VietCPQ94/react-signify/HEAD/src/app/public/logo512.png -------------------------------------------------------------------------------- /src/app-ssr/src/app/store.ts: -------------------------------------------------------------------------------- 1 | import { signify } from 'react-signify'; 2 | 3 | export const sCount = signify({ 4 | count: 0 5 | }); 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "tabWidth": 4, 5 | "trailingComma": "none", 6 | "arrowParens": "avoid", 7 | "printWidth": 200 8 | } 9 | -------------------------------------------------------------------------------- /src/app-ssr/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /src/package/index.ts: -------------------------------------------------------------------------------- 1 | export { signify } from './signify-core'; 2 | export type { TSignifyConfig } from './signify-core/signify.model'; 3 | export { type TCacheConfig as TCacheInfo } from './signify-cache/cache.model'; 4 | -------------------------------------------------------------------------------- /src/app-ssr/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function RootLayout({ 2 | children 3 | }: Readonly<{ 4 | children: React.ReactNode; 5 | }>) { 6 | return ( 7 | 8 | {children} 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/package/signify-cache/cache.model.ts: -------------------------------------------------------------------------------- 1 | // Default : value only save in memory 2 | type TCacheType = 'LocalStorage' | 'SessionStorage'; 3 | 4 | export type TCacheConfig = { 5 | type?: TCacheType; 6 | key: string; 7 | }; 8 | 9 | export type TCacheSolution = { [key in TCacheType]: () => Storage }; 10 | -------------------------------------------------------------------------------- /src/app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | import App from './App'; 3 | import reportWebVitals from './reportWebVitals'; 4 | 5 | const root = ReactDOM.createRoot( 6 | document.getElementById('root') as HTMLElement 7 | ); 8 | root.render( 9 | 10 | ); 11 | 12 | reportWebVitals(); 13 | -------------------------------------------------------------------------------- /src/app-ssr/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { sCount } from './store'; 4 | 5 | export default function Home() { 6 | const count = sCount.use(n => n.count); 7 | return ( 8 |
9 | Count: {count} 10 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/app-ssr/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /src/app/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/app-ssr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-ssr", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "next": "15.3.2", 13 | "react": "^19.0.0", 14 | "react-dom": "^19.0.0", 15 | "react-signify": "^1.5.7" 16 | }, 17 | "devDependencies": { 18 | "@eslint/eslintrc": "^3", 19 | "@types/node": "^20", 20 | "@types/react": "^19", 21 | "@types/react-dom": "^19", 22 | "eslint": "^9", 23 | "eslint-config-next": "15.3.2", 24 | "typescript": "^5" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app-ssr/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "baseUrl": "." 19 | }, 20 | "include": ["src"], 21 | "exclude": ["src/app/**/*"] 22 | } 23 | -------------------------------------------------------------------------------- /src/app-ssr/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/package/signify-core/signify.model.ts: -------------------------------------------------------------------------------- 1 | import { TCacheConfig } from '../signify-cache/cache.model'; 2 | 3 | export type TSignifyConfig = { 4 | cache?: TCacheConfig; 5 | syncKey?: string; 6 | }; 7 | 8 | export type TSetterCallback = (pre: { value: T }) => void; 9 | 10 | export type TWrapProps = { children(value: T): React.JSX.Element }; 11 | 12 | export type TListeners = Set<(value: T) => void>; 13 | 14 | export type TGetValueCb = () => T; 15 | 16 | export type TConvertValueCb = (v: T) => P; 17 | 18 | export type TUseValueCb = (value: T) => void; 19 | 20 | export type TConditionUpdate = (pre: T, cur: T) => boolean; 21 | 22 | export type TConditionRendering = (value: T) => boolean; 23 | 24 | export type TOmitHtml = T extends string | number ? P : Omit; 25 | -------------------------------------------------------------------------------- /src/package/utils/cookies.ts: -------------------------------------------------------------------------------- 1 | function setCookie(cname: string, cvalue: string, exdays: number = 30) { 2 | const d = new Date(); 3 | d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000); 4 | let expires = 'expires=' + d.toUTCString(); 5 | document.cookie = cname + '=' + cvalue + ';' + expires + ';path=/'; 6 | } 7 | 8 | function getCookie(cname: string) { 9 | let name = cname + '=', 10 | decodedCookie = decodeURIComponent(document.cookie), 11 | ca = decodedCookie.split(';'); 12 | 13 | for (let i = 0; i < ca.length; i++) { 14 | let c = ca[i]; 15 | while (c.charAt(0) == ' ') { 16 | c = c.substring(1); 17 | } 18 | if (c.indexOf(name) == 0) { 19 | return c.substring(name.length, c.length); 20 | } 21 | } 22 | return null; 23 | } 24 | 25 | export { getCookie, setCookie }; 26 | -------------------------------------------------------------------------------- /src/package/utils/objectCompare.ts: -------------------------------------------------------------------------------- 1 | export const deepCompare = (a: any, b: any): boolean => { 2 | if (a === b) return true; 3 | if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return a !== a && b !== b; // NaN check 4 | 5 | if (a.constructor !== b.constructor) return false; 6 | 7 | // Array comparison 8 | if (Array.isArray(a)) { 9 | if (a.length !== b.length) return false; 10 | for (let i = a.length; i--; ) if (!deepCompare(a[i], b[i])) return false; 11 | return true; 12 | } 13 | 14 | // Special object types 15 | if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags; 16 | if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf(); 17 | if (a.toString !== Object.prototype.toString) return a.toString() === b.toString(); 18 | 19 | // Object comparison 20 | const keys = Object.keys(a); 21 | if (keys.length !== Object.keys(b).length) return false; 22 | 23 | return keys.every(key => Object.prototype.hasOwnProperty.call(b, key) && deepCompare(a[key], b[key])); 24 | }; 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Viet Cong Pham Quoc 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 | -------------------------------------------------------------------------------- /src/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/node": "^16.18.102", 7 | "@types/react": "^18.3.3", 8 | "@types/react-dom": "^18.3.0", 9 | "react": "^18.3.1", 10 | "react-dom": "^18.3.1", 11 | "react-scripts": "5.0.1", 12 | "react-signify": "^1.2.1", 13 | "typescript": "^4.9.5", 14 | "web-vitals": "^2.1.4" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "devDependencies": { 41 | "@testing-library/jest-dom": "^6.4.6", 42 | "@testing-library/react": "^16.0.0", 43 | "@testing-library/user-event": "^14.5.2", 44 | "@types/jest": "^29.5.12" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/package/signify-sync/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a synchronization system using BroadcastChannel. 3 | * This function allows you to establish communication between different browser contexts by sending messages 4 | * and synchronizing state changes. 5 | * 6 | * @template T - The type of data to be synchronized. 7 | * @param {Object} params - The parameters for the syncSystem function. 8 | * @param {string} params.key - A unique identifier for the broadcast channel. 9 | * @param {function} params.cb - A callback function that will be invoked 10 | * whenever a new message is received. 11 | * @returns 12 | * - An object containing the following methods: 13 | * - post(data: T): Sends the provided data to the BroadcastChannel. 14 | * - sync(getData: () => T): Synchronizes the current data with other contexts by posting the data when 15 | * a message is received on a global BroadcastChannel. 16 | */ 17 | export const syncSystem = ({ key, cb }: { key: string; cb(val: T): void }) => { 18 | const mainKey = `bc_${key}`, 19 | bc = new BroadcastChannel(mainKey); 20 | 21 | bc.onmessage = e => cb(e.data); 22 | 23 | return { 24 | post: (data: T) => { 25 | bc.postMessage(data); 26 | }, 27 | sync: (getData: () => T) => { 28 | const bcs = new BroadcastChannel(`bcs`); 29 | bcs.onmessage = e => mainKey === e.data && bc.postMessage(getData()); 30 | bcs.postMessage(mainKey); 31 | } 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /src/app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/package/signify-devTool/index.css: -------------------------------------------------------------------------------- 1 | .signify_popup { 2 | font-size: 12px; 3 | width: 300px; 4 | height: 300px; 5 | background-color: white; 6 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); 7 | position: fixed; 8 | display: block; 9 | top: 50%; 10 | left: 50%; 11 | transform: translate(-50%, -50%); 12 | box-sizing: border-box; 13 | border-radius: 10px; 14 | overflow: hidden; 15 | } 16 | .signify_popup_header { 17 | cursor: move; 18 | display: flex; 19 | align-items: center; 20 | padding: 5px 20px; 21 | gap: 10px; 22 | font-size: 16px; 23 | color: white; 24 | user-select: none; 25 | } 26 | .signify_popup_header_button { 27 | cursor: pointer; 28 | } 29 | .signify_popup_header_label { 30 | margin-right: auto; 31 | overflow: hidden; 32 | text-overflow: ellipsis; 33 | text-wrap: nowrap; 34 | } 35 | .signify_popup_resizer { 36 | width: 20px; 37 | height: 20px; 38 | background-color: rgba(0, 0, 0, 0.1); 39 | position: absolute; 40 | bottom: 0; 41 | right: 0; 42 | cursor: se-resize; 43 | border-radius: 10px 0px 10px; 44 | } 45 | .signify_popup_json_viewer { 46 | margin: 0; 47 | height: calc(100% - 32px); 48 | overflow: auto; 49 | padding: 10px 20px; 50 | white-space: pre; 51 | box-sizing: border-box; 52 | } 53 | .signify_popup_json_viewer::-webkit-scrollbar { 54 | width: 8px; 55 | height: 8px; 56 | } 57 | .signify_popup_json_viewer::-webkit-scrollbar-thumb { 58 | background: gray; 59 | } 60 | .signify_popup_json_viewer::-webkit-scrollbar-track { 61 | background: #fff; 62 | } 63 | .signify_popup_json_key { 64 | color: brown; 65 | } 66 | .signify_popup_json_string { 67 | color: green; 68 | } 69 | .signify_popup_json_number { 70 | color: blue; 71 | } 72 | .signify_popup_json_boolean { 73 | color: purple; 74 | } 75 | .signify_popup_json_null { 76 | color: gray; 77 | } 78 | -------------------------------------------------------------------------------- /src/app/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /src/package/signify-devTool/index.scss: -------------------------------------------------------------------------------- 1 | .signify { 2 | &_popup { 3 | font-size: 12px; 4 | width: 300px; 5 | height: 300px; 6 | background-color: white; 7 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); 8 | position: fixed; 9 | display: block; 10 | top: 50%; 11 | left: 50%; 12 | transform: translate(-50%, -50%); 13 | box-sizing: border-box; 14 | border-radius: 10px; 15 | overflow: hidden; 16 | 17 | &_header { 18 | cursor: move; 19 | display: flex; 20 | align-items: center; 21 | padding: 5px 20px; 22 | gap: 10px; 23 | font-size: 16px; 24 | color: white; 25 | user-select: none; 26 | 27 | &_button { 28 | cursor: pointer; 29 | } 30 | 31 | &_label { 32 | margin-right: auto; 33 | overflow: hidden; 34 | text-overflow: ellipsis; 35 | text-wrap: nowrap; 36 | } 37 | } 38 | 39 | &_resizer { 40 | width: 20px; 41 | height: 20px; 42 | background-color: rgba(0, 0, 0, 0.1); 43 | position: absolute; 44 | bottom: 0; 45 | right: 0; 46 | cursor: se-resize; 47 | border-radius: 10px 0px 10px; 48 | } 49 | 50 | &_json { 51 | &_viewer { 52 | margin: 0; 53 | height: calc(100% - 32px); 54 | overflow: auto; 55 | padding: 10px 20px; 56 | white-space: pre; 57 | box-sizing: border-box; 58 | 59 | &::-webkit-scrollbar { 60 | width: 8px; 61 | height: 8px; 62 | } 63 | 64 | &::-webkit-scrollbar-thumb { 65 | background: gray; 66 | } 67 | 68 | &::-webkit-scrollbar-track { 69 | background: #fff; 70 | } 71 | } 72 | 73 | &_key { 74 | color: brown; 75 | } 76 | &_string { 77 | color: green; 78 | } 79 | &_number { 80 | color: blue; 81 | } 82 | &_boolean { 83 | color: purple; 84 | } 85 | &_null { 86 | color: gray; 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/package/signify-cache/index.ts: -------------------------------------------------------------------------------- 1 | import { TCacheConfig, TCacheSolution } from './cache.model'; 2 | 3 | // Define a cache solution object that maps cache types to their respective storage mechanisms 4 | const cacheSolution: TCacheSolution = { 5 | LocalStorage: () => localStorage, // Using the localStorage API for persistent storage 6 | SessionStorage: () => sessionStorage // Using the sessionStorage API for temporary storage 7 | }; 8 | 9 | /** 10 | * Retrieves the initial value from either LocalStorage or SessionStorage based on the provided cache configuration. 11 | * If the value is not found in the specified storage, it sets the initial value in the storage. 12 | * 13 | * @param initialValue - The initial value to be stored if no cached value exists. 14 | * @param cacheInfo - An optional configuration object that includes: 15 | * - key: The key under which the value is stored in the cache. 16 | * - type: The type of storage to use ('LocalStorage' or 'SesionStorage'). 17 | * @returns The retrieved value from cache or the initial value if no cached value exists. 18 | */ 19 | export const getInitialValue = (initialValue: T, cacheInfo?: TCacheConfig): T => { 20 | if (cacheInfo?.key) { 21 | if (typeof window === 'undefined') { 22 | throw new Error('The cache feature is not recommended for Server-Side Rendering. Please remove the cache properties from the Signify variable.'); 23 | } 24 | 25 | const mainType = cacheInfo?.type ?? 'LocalStorage', // Default to LocalStorage if no type is provided 26 | tempValue = cacheSolution[mainType]().getItem(cacheInfo.key); // Retrieve item from storage 27 | 28 | if (tempValue) { 29 | return JSON.parse(tempValue); // Return parsed value if found in storage 30 | } 31 | 32 | // Set initial value in storage if not found 33 | cacheSolution[mainType]().setItem(cacheInfo.key, JSON.stringify(initialValue)); 34 | } 35 | 36 | return initialValue; // Return a deep copy of the initial value 37 | }; 38 | 39 | /** 40 | * Updates the stored value in the specified cache configuration. 41 | * If the key is provided in cacheInfo, it updates the corresponding storage with the new value. 42 | * 43 | * @param newValue - The new value to be stored in the cache. 44 | * @param cacheInfo - An optional configuration object that includes: 45 | * - key: The key under which the new value will be stored. 46 | * - type: The type of storage to use ('LocalStorage' or 'SesionStorage'). 47 | */ 48 | export const cacheUpdateValue = (newValue: T, cacheInfo?: TCacheConfig) => { 49 | if (cacheInfo?.key) { 50 | // Update item in specified storage 51 | cacheSolution[cacheInfo?.type ?? 'LocalStorage']().setItem(cacheInfo.key, JSON.stringify(newValue)); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-signify", 3 | "description": "A JS library for predictable and maintainable global state management", 4 | "version": "1.7.2", 5 | "type": "module", 6 | "homepage": "https://reactsignify.dev", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/VietCPQ94/react-signify.git" 10 | }, 11 | "bugs": { 12 | "url": "github:VietCPQ94/react-signify/issues" 13 | }, 14 | "keywords": [ 15 | "react", 16 | "react-signify", 17 | "signal", 18 | "state", 19 | "global state", 20 | "manage state", 21 | "react-signal", 22 | "signals-react", 23 | "re-render" 24 | ], 25 | "authors": "Viet Cong Pham Quoc (https://github.com/VietCPQ94)", 26 | "main": "dist/cjs/index.js", 27 | "module": "dist/index.js", 28 | "types": "dist/index.d.ts", 29 | "scripts": { 30 | "publish": "npm publish --access=public", 31 | "app-start": "yarn --cwd src/app start", 32 | "app-start-ssr": "yarn --cwd src/app-ssr dev", 33 | "app-test": "yarn --cwd src/app test --watchAll", 34 | "build": "rollup -c --bundleConfigAsCjs", 35 | "test": "yarn build && cp -r dist src/app/node_modules/react-signify && cp package.json src/app/node_modules/react-signify/ && rm -rf src/app/node_modules/.cache && yarn app-test", 36 | "start": "yarn build && cp -r dist src/app/node_modules/react-signify && cp package.json src/app/node_modules/react-signify/ && rm -rf src/app/node_modules/.cache && yarn app-start", 37 | "start-ssr": "yarn build && cp -r dist src/app-ssr/node_modules/react-signify && cp package.json src/app-ssr/node_modules/react-signify/ && rm -rf src/app-ssr/.next && yarn app-start-ssr", 38 | "prepack": "yarn build" 39 | }, 40 | "author": "Viet Cong Pham Quoc", 41 | "license": "MIT", 42 | "exports": { 43 | "./package.json": "./package.json", 44 | ".": { 45 | "types": "./dist/index.d.ts", 46 | "import": "./dist/index.js", 47 | "default": "./dist/cjs/index.js" 48 | }, 49 | "./devtool": { 50 | "types": "./dist/devtool.d.ts", 51 | "import": "./dist/devtool.js", 52 | "require": "./dist/cjs/devtool.js" 53 | } 54 | }, 55 | "files": [ 56 | "dist/*" 57 | ], 58 | "devDependencies": { 59 | "@rollup/plugin-alias": "^5.1.0", 60 | "@rollup/plugin-commonjs": "^25.0.8", 61 | "@rollup/plugin-node-resolve": "^15.2.3", 62 | "@rollup/plugin-terser": "^0.4.4", 63 | "@rollup/plugin-typescript": "^11.1.6", 64 | "@types/react": "^18.3.2", 65 | "rollup": "^4.17.2", 66 | "rollup-plugin-copy": "^3.5.0", 67 | "rollup-plugin-dts": "^6.1.1", 68 | "rollup-plugin-peer-deps-external": "^2.2.4", 69 | "rollup-plugin-postcss": "^4.0.2", 70 | "tslib": "^2.6.2", 71 | "typescript": "^5.4.5" 72 | }, 73 | "peerDependencies": { 74 | "react": ">=17.0.2" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/package/utils/objectClone.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Deep clone any JavaScript object with support for circular references and custom constructors 3 | * @param source The object to be cloned 4 | * @param options Optional configuration 5 | * @returns A deep clone of the source object 6 | */ 7 | export const deepClone = ( 8 | source: T, 9 | options?: { 10 | constructorHandlers?: [Function, (obj: any, fn?: (o: any) => any) => any][]; 11 | } 12 | ): T => { 13 | // Handle primitives, null, and undefined directly 14 | if (typeof source !== 'object' || source === null) { 15 | return source; 16 | } 17 | 18 | // Create a WeakMap to track already cloned objects (for circular references) 19 | const cloneMap = new WeakMap(); 20 | 21 | // Built-in object handlers 22 | const handlers = new Map any) => any>([ 23 | [Date, (obj: Date) => new Date(obj.getTime())], 24 | [RegExp, (obj: RegExp) => new RegExp(obj.source, obj.flags)], 25 | [Map, (obj: Map, fn?: (o: any) => any) => new Map([...obj].map(([k, v]) => [fn!(k), fn!(v)]))], 26 | [Set, (obj: Set, fn?: (o: any) => any) => new Set([...obj].map(fn!))], 27 | [Error, (obj: Error, fn?: (o: any) => any) => Object.assign(new (obj.constructor as any)(obj.message), { stack: obj.stack, cause: obj.cause && fn!(obj.cause) })], 28 | [URL, (obj: URL) => new URL(obj.href)], 29 | [Blob, (obj: Blob) => obj.slice()], 30 | [File, (obj: File) => new File([obj], obj.name, { type: obj.type, lastModified: obj.lastModified })], 31 | ...(options?.constructorHandlers || []) 32 | ]); 33 | 34 | // Main clone function 35 | const clone = (obj: any): any => { 36 | // Handle primitives 37 | if (typeof obj !== 'object' || obj === null) return obj; 38 | 39 | // Handle circular references 40 | if (cloneMap.has(obj)) return cloneMap.get(obj); 41 | 42 | let result: any; 43 | 44 | // Handle different object types 45 | if (Array.isArray(obj)) { 46 | result = obj.map(clone); 47 | } else if (ArrayBuffer.isView(obj)) { 48 | result = obj instanceof Buffer ? Buffer.from(obj) : new (obj.constructor as any)(obj.buffer.slice(), obj.byteOffset, (obj as any).length); 49 | } else if (handlers.has(obj.constructor)) { 50 | result = handlers.get(obj.constructor)!(obj, clone); 51 | } else { 52 | // Handle plain objects 53 | result = Object.create(Object.getPrototypeOf(obj)); 54 | cloneMap.set(obj, result); 55 | 56 | // Copy all properties in one pass 57 | [...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj)].forEach(key => { 58 | const descriptor = Object.getOwnPropertyDescriptor(obj, key)!; 59 | if (descriptor.value !== undefined) descriptor.value = clone(descriptor.value); 60 | Object.defineProperty(result, key, descriptor); 61 | }); 62 | return result; 63 | } 64 | 65 | cloneMap.set(obj, result); 66 | return result; 67 | }; 68 | 69 | return clone(source); 70 | }; 71 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when there is a 56 | reasonable belief that an individual's behavior may have a negative impact on 57 | the project or its community. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported by contacting admin at . All 63 | complaints will be reviewed and investigated and will result in a response that 64 | is deemed necessary and appropriate to the circumstances. The project team is 65 | obligated to maintain confidentiality with regard to the reporter of an incident. 66 | Further details of specific enforcement policies may be posted separately. 67 | 68 | Project maintainers who do not follow or enforce the Code of Conduct in good 69 | faith may face temporary or permanent repercussions as determined by other 70 | members of the project's leadership. 71 | 72 | ## Attribution 73 | 74 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 75 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 76 | 77 | [homepage]: https://www.contributor-covenant.org 78 | 79 | For answers to common questions about this code of conduct, see 80 | https://www.contributor-covenant.org/faq 81 | -------------------------------------------------------------------------------- /src/package/signify-core/signify.core.ts: -------------------------------------------------------------------------------- 1 | import React, { DependencyList, memo, useLayoutEffect, useRef, useState } from 'react'; 2 | import { deepCompare } from '../utils/objectCompare'; 3 | import { TConvertValueCb, TGetValueCb, TListeners, TUseValueCb, TWrapProps } from './signify.model'; 4 | 5 | export const subscribeCore = 6 | (listeners: TListeners) => 7 | (callback: TUseValueCb) => { 8 | listeners.add(callback); 9 | return { unsubscribe: () => listeners.delete(callback) }; 10 | }; 11 | 12 | /** 13 | * watchCore is a custom hook that subscribes to a set of listeners. 14 | * It allows the provided callback to be invoked whenever the state changes. 15 | * The listeners will be cleaned up when the component unmounts or dependencies change. 16 | * 17 | * @param listeners - A collection of listeners for state changes. 18 | * @returns A function that takes a callback and an optional dependency array. 19 | */ 20 | export const watchCore = 21 | (listeners: TListeners) => 22 | (callback: TUseValueCb, deps?: DependencyList) => { 23 | useLayoutEffect(() => { 24 | listeners.add(callback); 25 | 26 | return () => { 27 | listeners.delete(callback); 28 | }; 29 | }, deps ?? []); 30 | }; 31 | 32 | /** 33 | * useCore is a custom hook that retrieves a value from the state and triggers 34 | * a re-render when the value changes. It allows you to transform the retrieved 35 | * value using a provided conversion function. 36 | * 37 | * @param listeners - A collection of listeners for state changes. 38 | * @param getValue - A function to retrieve the current value from the state. 39 | * @returns A function that takes an optional value conversion function. v => v as Readonly

40 | */ 41 | export const useCore = 42 | (listeners: TListeners, getValue: () => T) => 43 |

(pickValue?: TConvertValueCb) => { 44 | const trigger = useState({})[1]; 45 | const listener = useRef( 46 | (() => { 47 | let temp = pickValue?.(getValue()); 48 | const listenerFunc = () => { 49 | if (pickValue) { 50 | let newTemp = pickValue(getValue()); 51 | 52 | if (deepCompare(temp, newTemp)) { 53 | return; 54 | } 55 | temp = newTemp; 56 | } 57 | 58 | trigger({}); 59 | }; 60 | return listenerFunc; 61 | })() 62 | ); 63 | 64 | useLayoutEffect(() => { 65 | listeners.add(listener.current); 66 | return () => { 67 | listeners.delete(listener.current); 68 | }; 69 | }, []); 70 | 71 | return (pickValue ? pickValue(getValue()) : getValue()) as P extends undefined ? T : P; 72 | }; 73 | 74 | /** 75 | * htmlCore is a utility function that creates a React element by invoking 76 | * the provided value retrieval function. This is useful for rendering 77 | * values directly in a functional component. 78 | * 79 | * @param u - A function that retrieves the current value. 80 | * @returns A React element containing the rendered value. 81 | */ 82 | //@ts-ignore 83 | export const htmlCore = (u: TGetValueCb) => React.createElement(() => u()); 84 | 85 | /** 86 | * WrapCore is a higher-order component that wraps its children with the 87 | * current value retrieved from the provided function. 88 | * 89 | * @param u - A function that retrieves the current value. 90 | * @returns A component that renders its children with the current value. 91 | * 92 | * @example 93 | * const getValue = () => 'Wrapped Value'; 94 | * const WrappedComponent = WrapCore(getValue); 95 | */ 96 | export const WrapCore = 97 | (u: TGetValueCb) => 98 | ({ children }: TWrapProps) => 99 | children(u()); 100 | 101 | /** 102 | * HardWrapCore is a memoized version of WrapCore that optimizes rendering 103 | * by preventing unnecessary updates. It uses shallow comparison to determine 104 | * if the component should re-render. 105 | * 106 | * @param u - A function that retrieves the current value. 107 | * @returns A memoized component that wraps its children with the current value. 108 | */ 109 | export const HardWrapCore = (u: TGetValueCb) => memo(WrapCore(u), () => true) as ReturnType>; 110 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import typescript from '@rollup/plugin-typescript'; 4 | import dts from 'rollup-plugin-dts'; 5 | import terser from '@rollup/plugin-terser'; 6 | import peerDepsExternal from 'rollup-plugin-peer-deps-external'; 7 | import postcss from 'rollup-plugin-postcss'; 8 | import alias from '@rollup/plugin-alias'; 9 | import path from 'path'; 10 | 11 | const packageJson = require('./package.json'); 12 | 13 | export default [ 14 | // Build main index file 15 | { 16 | input: 'src/package/index.ts', 17 | output: [ 18 | { 19 | dir: 'dist/cjs', 20 | format: 'cjs', 21 | sourcemap: true, 22 | entryFileNames: 'index.js' 23 | }, 24 | { 25 | dir: 'dist', 26 | format: 'esm', 27 | sourcemap: true, 28 | entryFileNames: 'index.js' 29 | } 30 | ], 31 | plugins: [ 32 | alias({ 33 | entries: [ 34 | { find: 'react', replacement: path.resolve(__dirname, 'node_modules/react') }, 35 | { find: 'react-dom', replacement: path.resolve(__dirname, 'node_modules/react-dom') } 36 | ] 37 | }), 38 | resolve({ 39 | extensions: ['.js', '.jsx', '.ts', '.tsx'] 40 | }), 41 | commonjs({ 42 | include: /node_modules/ 43 | }), 44 | peerDepsExternal(), 45 | typescript({ tsconfig: './tsconfig.json' }), 46 | terser({ 47 | output: { 48 | comments: false 49 | }, 50 | compress: { 51 | drop_console: true, 52 | drop_debugger: true, 53 | pure_funcs: ['console.info', 'console.debug', 'console.warn'], 54 | passes: 3, 55 | dead_code: true, 56 | keep_fargs: false, 57 | keep_fnames: false 58 | } 59 | }) 60 | ], 61 | external: ['react', 'react-dom'] 62 | }, 63 | // Build DevTool file separately 64 | { 65 | input: 'src/package/signify-devTool/index.tsx', 66 | output: [ 67 | { 68 | dir: 'dist/cjs', 69 | format: 'cjs', 70 | sourcemap: true, 71 | entryFileNames: 'devtool.js' 72 | }, 73 | { 74 | dir: 'dist', 75 | format: 'esm', 76 | sourcemap: true, 77 | entryFileNames: 'devtool.js' 78 | } 79 | ], 80 | plugins: [ 81 | alias({ 82 | entries: [ 83 | { find: 'react', replacement: path.resolve(__dirname, 'node_modules/react') }, 84 | { find: 'react-dom', replacement: path.resolve(__dirname, 'node_modules/react-dom') } 85 | ] 86 | }), 87 | resolve({ 88 | extensions: ['.js', '.jsx', '.ts', '.tsx'] 89 | }), 90 | commonjs({ 91 | include: /node_modules/ 92 | }), 93 | peerDepsExternal(), 94 | typescript({ tsconfig: './tsconfig.json' }), 95 | terser({ 96 | output: { 97 | comments: false 98 | }, 99 | compress: { 100 | drop_console: true, 101 | drop_debugger: true, 102 | pure_funcs: ['console.info', 'console.debug', 'console.warn'], 103 | passes: 3, 104 | dead_code: true, 105 | keep_fargs: false, 106 | keep_fnames: false 107 | } 108 | }), 109 | postcss() 110 | ], 111 | external: ['react', 'react-dom'] 112 | }, 113 | // Generate type definitions for main index 114 | { 115 | input: 'src/package/index.ts', 116 | output: [ 117 | { 118 | dir: 'dist', 119 | format: 'cjs', 120 | entryFileNames: 'index.d.ts' 121 | } 122 | ], 123 | plugins: [dts.default()], 124 | external: [/\.css$/] 125 | }, 126 | // Generate type definitions for DevTool 127 | { 128 | input: 'src/package/signify-devTool/index.tsx', 129 | output: [ 130 | { 131 | dir: 'dist', 132 | format: 'cjs', 133 | entryFileNames: 'devtool.d.ts' 134 | } 135 | ], 136 | plugins: [dts.default()], 137 | external: [/\.css$/, 'react', 'react-dom'] 138 | } 139 | ]; 140 | -------------------------------------------------------------------------------- /src/package/signify-devTool/index.tsx: -------------------------------------------------------------------------------- 1 | import { ElementRef, memo, MouseEvent, useCallback, useEffect, useLayoutEffect, useRef } from 'react'; 2 | import { getCookie, setCookie } from '../utils/cookies'; 3 | import './index.css'; 4 | 5 | let index = 100; 6 | 7 | const getRandomPastelColor = () => { 8 | const r = Math.floor(Math.random() * 128), 9 | g = Math.floor(Math.random() * 128), 10 | b = Math.floor(Math.random() * 128); 11 | return `rgb(${r}, ${g}, ${b})`; 12 | }; 13 | 14 | type TDevtool = { name: string; color?: string; item: any }; 15 | 16 | export const DevTool = memo( 17 | ({ name, color, item }: TDevtool) => { 18 | const popup = useRef(null); 19 | let nameCookies = `rs-${name}`, 20 | isDragging = false, 21 | isResizing = false, 22 | offsetX = 0, 23 | offsetY = 0, 24 | renderCount = 0; 25 | 26 | useLayoutEffect(() => { 27 | if (popup.current) { 28 | const { x, y, h, w }: { [key: string]: string } = JSON.parse(getCookie(nameCookies) ?? '{}'); 29 | x && (popup.current.style.left = x); 30 | y && (popup.current.style.top = y); 31 | w && (popup.current.style.width = w); 32 | h && (popup.current.style.height = h); 33 | } 34 | }, []); 35 | 36 | useEffect(() => { 37 | const mouseMove = (e: globalThis.MouseEvent) => { 38 | if (isDragging && popup.current) { 39 | popup.current.style.left = `${e.clientX - offsetX}px`; 40 | popup.current.style.top = `${e.clientY - offsetY}px`; 41 | } 42 | 43 | if (isResizing && popup.current) { 44 | const rect = popup.current.getBoundingClientRect(); 45 | 46 | const newWidth = e.clientX - rect.left, 47 | newHeight = e.clientY - rect.top; 48 | 49 | if (newWidth > 100) { 50 | popup.current.style.width = `${newWidth + 10}px`; 51 | } 52 | 53 | if (newHeight > 100) { 54 | popup.current.style.height = `${newHeight + 10}px`; 55 | } 56 | } 57 | }; 58 | 59 | const mouseUp = () => { 60 | isDragging = false; 61 | isResizing = false; 62 | document.body.style.cursor = 'default'; 63 | if (popup.current) { 64 | setCookie( 65 | nameCookies, 66 | JSON.stringify({ 67 | x: popup.current.style.left, 68 | y: popup.current.style.top, 69 | w: popup.current.style.width, 70 | h: popup.current.style.height 71 | }) 72 | ); 73 | } 74 | }; 75 | 76 | document.addEventListener('mousemove', mouseMove); 77 | document.addEventListener('mouseup', mouseUp); 78 | 79 | return () => { 80 | document.removeEventListener('mousemove', mouseMove); 81 | document.removeEventListener('mouseup', mouseUp); 82 | }; 83 | }, []); 84 | 85 | const headerMouseDown = useCallback(({ clientX, clientY }: { clientX: number; clientY: number }) => { 86 | if (popup.current) { 87 | isDragging = true; 88 | popup.current.style.zIndex = `${index++}`; 89 | offsetX = clientX - popup.current?.offsetLeft; 90 | offsetY = clientY - popup.current?.offsetTop; 91 | } 92 | }, []); 93 | 94 | const resizeMouseDown = useCallback((e: MouseEvent>) => { 95 | isResizing = true; 96 | document.body.style.cursor = 'se-resize'; 97 | e.preventDefault(); 98 | }, []); 99 | 100 | const handleFontSize = useCallback( 101 | (isUp: boolean) => () => { 102 | if (popup.current) { 103 | if (!popup.current.style.fontSize) { 104 | popup.current.style.fontSize = '12px'; 105 | } 106 | popup.current.style.fontSize = Number(popup.current.style.fontSize.replace('px', '')) + (isUp ? 2 : -2) + 'px'; 107 | } 108 | }, 109 | [] 110 | ); 111 | 112 | const syntaxHighlight = useCallback((json: string) => { 113 | if (typeof json != 'string') { 114 | json = JSON.stringify(json, undefined, 2); 115 | } 116 | json = json.replace(/&/g, '&').replace(//g, '>'); 117 | // eslint-disable-next-line 118 | return json.replace(/("(\\u[a-zA-Z0-9]{4}|\ $^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { 119 | let cls = 'number'; 120 | if (/^"/.test(match)) { 121 | if (/:$/.test(match)) { 122 | cls = 'key'; 123 | } else { 124 | cls = 'string'; 125 | } 126 | } else if (/true|false/.test(match)) { 127 | cls = 'boolean'; 128 | } else if (/null/.test(match)) { 129 | cls = 'null'; 130 | } 131 | return '' + match + ''; 132 | }); 133 | }, []); 134 | 135 | return ( 136 |

137 |
138 | 147 | 148 | 149 |
150 | {(n: any) =>
}
151 | 152 |
153 |
154 | ); 155 | }, 156 | () => true 157 | ); 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Signify 2 | 3 | ![image](https://files.notice.studio/workspaces/d8b84700-32ef-4e9d-9d5e-3eebb0e5e197/ee5da14e-f977-4016-a664-4169e7888ccf.png) 4 | 5 | # Introduction 6 | 7 | React Signify is a simple library that provides features for managing and updating global state efficiently. It is particularly useful in React applications for managing state and auto-syncing when their values change. 8 | Advantages of the library: 9 | 10 | - Lightweight library 11 | - Simple syntax 12 | - Supports effective re-render control 13 | 14 | # Project information 15 | 16 | - Git: https://github.com/VietCPQ94/react-signify 17 | - NPM: [https://www.npmjs.com/package/react-signify](https://www.npmjs.com/package/react-signify) 18 | 19 | # Installation 20 | 21 | React Signify is available as a package on NPM for use with React applications: 22 | 23 | ``` 24 | # NPM 25 | npm install react-signify 26 | 27 | # Yarn 28 | yarn add react-signify 29 | ``` 30 | 31 | # Overview 32 | 33 | ## Initialize 34 | 35 | You can initialize Signify in any file, please refer to the following example 36 | 37 | ```tsx 38 | import { signify } from 'react-signify'; 39 | 40 | const sCount = signify(0); 41 | ``` 42 | 43 | Here we create a variable `sCount` with an initial value of `0`. 44 | 45 | ## Used in many places 46 | 47 | The usage is simple with the export/import tool of the module. 48 | File Store.js (export Signify) 49 | 50 | ```tsx 51 | import { signify } from 'react-signify'; 52 | 53 | export const sCount = signify(0); 54 | ``` 55 | 56 | Component A (import Signify) 57 | 58 | ```tsx 59 | import { sCount } from './store'; 60 | 61 | export default function ComponentA() { 62 | const countValue = sCount.use(); 63 | const handleUp = () => { 64 | sCount.set(pre => { 65 | pre.value += 1; 66 | }); 67 | }; 68 | 69 | return ( 70 |
71 |

{countValue}

72 | 73 |
74 | ); 75 | } 76 | ``` 77 | 78 | From here we can see the flexibility of Signify, simple declaration, usable everywhere. 79 | 80 | ## Basic features 81 | 82 | ### Display on the interface 83 | 84 | We will use the `html` attribute to display the value as a `string` or `number` on the interface. 85 | 86 | ```tsx 87 | import { signify } from 'react-signify'; 88 | 89 | const sCount = signify(0); 90 | 91 | export default function App() { 92 | return ( 93 |
94 |

{sCount.html}

95 |
96 | ); 97 | } 98 | ``` 99 | 100 | ### Update value 101 | 102 | ```tsx 103 | import { signify } from 'react-signify'; 104 | 105 | const sCount = signify(0); 106 | 107 | export default function App() { 108 | const handleSet = () => { 109 | sCount.set(1); 110 | }; 111 | 112 | const handleUp = () => { 113 | sCount.set(pre => { 114 | pre.value += 1; 115 | }); 116 | }; 117 | 118 | return ( 119 |
120 |

{sCount.html}

121 | 122 | 123 |
124 | ); 125 | } 126 | ``` 127 | 128 | Pressing the button will change the value of Signify and will be automatically updated on the interface. 129 | 130 | ## Advanced features 131 | 132 | ### Use 133 | 134 | The feature allows retrieving the value of Signify and using it as a state of the component. 135 | 136 | ```tsx 137 | import { signify } from 'react-signify'; 138 | 139 | const sCount = signify(0); 140 | 141 | export default function App() { 142 | const countValue = sCount.use(); 143 | const handleUp = () => { 144 | sCount.set(pre => { 145 | pre.value += 1; 146 | }); 147 | }; 148 | 149 | return ( 150 |
151 |

{countValue}

152 | 153 |
154 | ); 155 | } 156 | ``` 157 | 158 | ### watch 159 | 160 | The feature allows for safe tracking of the value changes of Signify. 161 | 162 | ```tsx 163 | import { signify } from 'react-signify'; 164 | 165 | const sCount = signify(0); 166 | 167 | export default function App() { 168 | const handleUp = () => { 169 | sCount.set(pre => { 170 | pre.value += 1; 171 | }); 172 | }; 173 | 174 | sCount.watch(value => { 175 | console.log(value); 176 | }); 177 | 178 | return ( 179 |
180 | 181 |
182 | ); 183 | } 184 | ``` 185 | 186 | ### Wrap 187 | 188 | The feature applies the value of Signify in a specific interface area. 189 | 190 | ```tsx 191 | import { signify } from 'react-signify'; 192 | 193 | const sCount = signify(0); 194 | 195 | export default function App() { 196 | const handleUp = () => { 197 | sCount.set(pre => { 198 | pre.value += 1; 199 | }); 200 | }; 201 | return ( 202 |
203 | 204 | {value => ( 205 |
206 |

{value}

207 |
208 | )} 209 |
210 | 211 |
212 | ); 213 | } 214 | ``` 215 | 216 | ### Hardwrap 217 | 218 | The feature applies the value of Signify in a specific interface area and limits unnecessary re-renders when the parent component re-renders. 219 | 220 | ```tsx 221 | import { signify } from 'react-signify'; 222 | 223 | const sCount = signify(0); 224 | 225 | export default function App() { 226 | const handleUp = () => { 227 | sCount.set(pre => { 228 | pre.value += 1; 229 | }); 230 | }; 231 | return ( 232 |
233 | 234 | {value => ( 235 |
236 |

{value}

237 |
238 | )} 239 |
240 | 241 |
242 | ); 243 | } 244 | ``` 245 | 246 | ### reset 247 | 248 | A tool that allows restoring the default value. Helps free up resources when no longer in use. 249 | 250 | ```tsx 251 | import { signify } from 'react-signify'; 252 | 253 | const sCount = signify(0); 254 | 255 | sCount.reset(); 256 | ``` 257 | 258 | # See more 259 | 260 | - [Reference API](https://reactsignify.dev?page=178ffe42-6184-4973-8c66-4990023792cb) 261 | - [Render & Update](https://reactsignify.dev?page=6fea6251-87d1-4066-97a1-ff3393ded797) 262 | - [Usage with TypeScript](https://reactsignify.dev?page=ecc96837-657b-4a13-9001-d81262ae78d8) 263 | - [Devtool](https://reactsignify.dev?page=e5e11cc8-10a6-4979-90a4-a310e9f5c8b8) 264 | - [Style Guide](https://reactsignify.dev?page=074944b4-eb6c-476f-b293-e8768f45e5dc) 265 | - [Structure](https://reactsignify.dev?page=159467bd-4bed-4d5f-af11-3b9bb20fc9d6) 266 | - [Understand Signify](https://reactsignify.dev?page=a022737b-5f0e-47a5-990f-fa9a3b62662d) 267 | -------------------------------------------------------------------------------- /src/app/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup, findByTestId, fireEvent, getByTestId, render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | const keys = [ 5 | 'btn-set', 6 | 'btn-stop', 7 | 'btn-resume', 8 | 'btn-reset', 9 | 'btn-countEnableConditionRender', 10 | 'btn-countDisableConditionRender', 11 | 'btn-countEnableConditionUpdate', 12 | 'btn-countDisableConditionUpdate', 13 | 'p-html', 14 | 'p-value', 15 | 'p-use', 16 | 'p-wrap', 17 | 'p-hardwrap', 18 | 'btn-setAge', 19 | 'btn-resetUser', 20 | 'btn-stopAge', 21 | 'btn-resumeAge', 22 | 'btn-ageEnableConditionRender', 23 | 'btn-ageDisableConditionRender', 24 | 'ps-html', 25 | 'ps-value', 26 | 'ps-use', 27 | 'ps-wrap', 28 | 'ps-hardwrap', 29 | 'pu-wrap', 30 | 'pu-hardwrap', 31 | 'btnu-set', 32 | 'btnu-stop', 33 | 'btnu-resume', 34 | 'btnu-reset', 35 | 'btnu-countEnableConditionRender', 36 | 'btnu-countDisableConditionRender', 37 | 'btnu-countEnableConditionUpdate', 38 | 'btnu-countDisableConditionUpdate', 39 | 'pu-value', 40 | 'pu-use', 41 | 'pu-wrap', 42 | 'pu-hardwrap', 43 | 'puw-watch', 44 | 'btnArr-set', 45 | 'btnArr-stop', 46 | 'btnArr-resume', 47 | 'btnArr-reset', 48 | 'btnArr-countEnableConditionRender', 49 | 'btnArr-countDisableConditionRender', 50 | 'btnArr-countEnableConditionUpdate', 51 | 'btnArr-countDisableConditionUpdate', 52 | 'parr-valuePick', 53 | 'parr-value', 54 | 'parr-use', 55 | 'parr-wrap', 56 | 'parr-hardwrap' 57 | ] as const; 58 | 59 | const checkCount = (value = '0') => { 60 | Object.values(keys) 61 | .filter(n => n.includes('p-')) 62 | .forEach(n => { 63 | expect(screen.getByTestId(n).innerHTML).toEqual(value); 64 | }); 65 | }; 66 | 67 | const checkAge = (value = '27') => { 68 | Object.values(keys) 69 | .filter(n => n.includes('ps-')) 70 | .forEach(n => { 71 | expect(screen.getByTestId(n).innerHTML).toEqual(value); 72 | }); 73 | }; 74 | 75 | const checkUser = (value = '27') => { 76 | Object.values(keys) 77 | .filter(n => n.includes('pu-')) 78 | .forEach(n => { 79 | expect(screen.getByTestId(n).innerHTML).toEqual(value); 80 | }); 81 | }; 82 | 83 | const checkArr = (value = '0') => { 84 | Object.values(keys) 85 | .filter(n => n.includes('parr-')) 86 | .forEach(n => { 87 | expect(screen.getByTestId(n).innerHTML).toEqual(value); 88 | }); 89 | }; 90 | 91 | const checkWatch = (id: 'psw-watch' | 'pw-watch' | 'parrw-watch') => { 92 | expect(screen.getByTestId(id).innerHTML).toEqual('OK'); 93 | }; 94 | 95 | const click = (id: (typeof keys)[number]) => { 96 | fireEvent.click(screen.getByTestId(id)); 97 | }; 98 | 99 | beforeEach(() => { 100 | cleanup(); 101 | render(); 102 | click('btn-reset'); 103 | click('btn-resetUser'); 104 | click('btnu-reset'); 105 | click('btnArr-reset'); 106 | }); 107 | 108 | describe('Normal Value Testing', () => { 109 | test('[sCount] Test Signify and all element init successfull', () => { 110 | checkCount(); 111 | }); 112 | 113 | test('[sCount] Test FireEvent set count', () => { 114 | checkCount(); 115 | click('btn-set'); 116 | checkCount('1'); 117 | }); 118 | 119 | test('[sCount] Test FireEvent stop/resume count', () => { 120 | checkCount(); 121 | click('btn-stop'); 122 | click('btn-set'); 123 | checkCount(); 124 | click('btn-resume'); 125 | checkCount('1'); 126 | }); 127 | 128 | test('[sCount] Test reset', () => { 129 | checkCount(); 130 | click('btn-set'); 131 | checkCount('1'); 132 | click('btn-reset'); 133 | checkCount(); 134 | }); 135 | 136 | test('[sCount] Test watch', () => { 137 | click('btn-set'); 138 | checkWatch('pw-watch'); 139 | }); 140 | 141 | test('[sCount] Test condition render', () => { 142 | click('btn-countEnableConditionRender'); 143 | click('btn-set'); 144 | checkCount(); 145 | click('btn-set'); 146 | checkCount(); 147 | click('btn-countDisableConditionRender'); 148 | click('btn-set'); 149 | checkCount('3'); 150 | }); 151 | 152 | test('[sCount] Test condition update', () => { 153 | click('btn-countEnableConditionUpdate'); 154 | click('btn-set'); 155 | checkCount('1'); 156 | click('btn-set'); 157 | checkCount('1'); 158 | click('btn-countDisableConditionUpdate'); 159 | click('btn-set'); 160 | checkCount('2'); 161 | }); 162 | }); 163 | 164 | describe('Object Value Testing', () => { 165 | test('[sUser] Test Signify and all element init successfull', () => { 166 | checkUser(); 167 | }); 168 | 169 | test('[sUser] Test FireEvent set count', () => { 170 | checkUser(); 171 | click('btnu-set'); 172 | checkUser('28'); 173 | }); 174 | 175 | test('[sUser] Test FireEvent stop/resume count', () => { 176 | checkUser(); 177 | click('btnu-stop'); 178 | click('btnu-set'); 179 | checkUser(); 180 | click('btnu-resume'); 181 | checkUser('28'); 182 | }); 183 | 184 | test('[sUser] Test reset', () => { 185 | checkUser(); 186 | click('btnu-set'); 187 | checkUser('28'); 188 | click('btnu-reset'); 189 | checkUser(); 190 | }); 191 | 192 | test('[sUser] Test watch', () => { 193 | click('btnu-set'); 194 | checkWatch('pw-watch'); 195 | }); 196 | 197 | test('[sUser] Test condition render', () => { 198 | checkUser(); 199 | click('btnu-countEnableConditionRender'); 200 | click('btnu-set'); 201 | checkUser('28'); 202 | click('btnu-set'); 203 | checkUser('28'); 204 | click('btnu-countDisableConditionRender'); 205 | click('btnu-set'); 206 | checkUser('30'); 207 | }); 208 | 209 | test('[sUser] Test condition update', () => { 210 | click('btnu-countEnableConditionUpdate'); 211 | click('btnu-set'); 212 | checkUser('27'); 213 | click('btnu-set'); 214 | checkUser('27'); 215 | click('btnu-countDisableConditionUpdate'); 216 | click('btnu-set'); 217 | checkUser('28'); 218 | }); 219 | }); 220 | 221 | describe('Slice Testing', () => { 222 | test('[ssAge] Test Signify and all element init successfull', () => { 223 | checkAge(); 224 | }); 225 | 226 | test('[ssAge] Test FireEvent stop/resume count', () => { 227 | checkAge(); 228 | click('btn-stopAge'); 229 | click('btn-setAge'); 230 | checkAge(); 231 | click('btn-resumeAge'); 232 | checkAge('28'); 233 | }); 234 | 235 | test('[ssAge] Test watch', () => { 236 | click('btn-setAge'); 237 | checkWatch('psw-watch'); 238 | }); 239 | 240 | test('[ssAge] Test condition render', () => { 241 | click('btn-ageEnableConditionRender'); 242 | checkAge('27'); 243 | click('btn-setAge'); 244 | checkAge('28'); 245 | click('btn-setAge'); 246 | checkAge('28'); 247 | click('btn-ageDisableConditionRender'); 248 | click('btn-setAge'); 249 | checkAge('30'); 250 | }); 251 | }); 252 | 253 | describe('Array Value Testing', () => { 254 | test('[sLs] Test Signify and all element init successfull', () => { 255 | checkArr(); 256 | }); 257 | 258 | test('[sLs] Test FireEvent set count', () => { 259 | checkArr(); 260 | click('btnArr-set'); 261 | checkArr('1'); 262 | }); 263 | 264 | test('[sLs] Test FireEvent stop/resume count', () => { 265 | checkArr(); 266 | click('btnArr-stop'); 267 | click('btnArr-set'); 268 | checkArr(); 269 | click('btnArr-resume'); 270 | checkArr('1'); 271 | }); 272 | 273 | test('[sLs] Test reset', () => { 274 | checkArr(); 275 | click('btnArr-set'); 276 | checkArr('1'); 277 | click('btnArr-reset'); 278 | checkArr(); 279 | }); 280 | 281 | test('[sLs] Test watch', () => { 282 | click('btnArr-set'); 283 | checkWatch('parrw-watch'); 284 | }); 285 | 286 | test('[sLs] Test condition render', () => { 287 | click('btnArr-countEnableConditionRender'); 288 | click('btnArr-set'); 289 | checkArr(); 290 | click('btnArr-set'); 291 | checkArr(); 292 | click('btnArr-countDisableConditionRender'); 293 | click('btnArr-set'); 294 | checkArr('3'); 295 | }); 296 | 297 | test('[sLs] Test condition update', () => { 298 | click('btnArr-countEnableConditionUpdate'); 299 | click('btnArr-set'); 300 | checkArr('1'); 301 | click('btnArr-set'); 302 | checkArr('1'); 303 | click('btnArr-countDisableConditionUpdate'); 304 | click('btnArr-set'); 305 | checkArr('2'); 306 | }); 307 | }); 308 | -------------------------------------------------------------------------------- /src/app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { signify } from 'react-signify'; 3 | 4 | export const sCount = signify(0); 5 | 6 | export const sUser = signify({ 7 | name: 'Viet', 8 | info: { 9 | age: 27, 10 | address: 'USA' 11 | } 12 | }); 13 | 14 | export const sLs = signify([{ count: 0 }]); 15 | 16 | const ssAge = sUser.slice(n => n.info.age); 17 | const ssInfo = sUser.slice(n => n.info); 18 | 19 | export default function App() { 20 | const count = sCount.use(); 21 | const age = ssAge.use(); 22 | const ageSlicePick = ssInfo.use(n => n.age); 23 | const user = sUser.use(); 24 | const agePick = sUser.use(n => n.info.age); 25 | const ls = sLs.use(); 26 | const arrayPick = sLs.use(n => n[0].count); 27 | const [isWatch, setIsWatch] = useState(false); 28 | const [isWatchSlice, setIsWatchSlice] = useState(false); 29 | const [isWatchUser, setIsWatchUser] = useState(false); 30 | const [isWatchLs, setIsWatchLs] = useState(false); 31 | 32 | sCount.watch(v => { 33 | setIsWatch(true); 34 | }); 35 | 36 | ssAge.watch(v => { 37 | setIsWatchSlice(true); 38 | }); 39 | 40 | sUser.watch(v => { 41 | setIsWatchUser(true); 42 | }); 43 | 44 | sLs.watch(v => { 45 | setIsWatchLs(true); 46 | }); 47 | 48 | return ( 49 | <> 50 |

Normal Case

51 |
52 | 62 | 65 | 68 | 76 | 79 | 82 | 85 | 88 |
89 |

{sCount.html}

90 |

{sCount.value}

91 |

{count}

92 | {n =>

{n}

}
93 | {n =>

{n}

}
94 |

{isWatch && 'OK'}

95 |
96 |

Slice

97 |
98 | 101 | 104 | 107 | 110 | 113 | 116 |
117 |

{ssAge.html}

118 |

{ssAge.value}

119 |

{age}

120 |

{ageSlicePick}

121 | {n =>

{n}

}
122 | {n =>

{n}

}
123 |

{isWatchSlice && 'OK'}

124 |
125 |

Object Case

126 |
127 | 130 | 133 | 136 | 139 | 142 | 145 | 148 | 151 |
152 | 153 |

{agePick}

154 |

{sUser.value.info.age}

155 |

{user.info.age}

156 | {n =>

{n.info.age}

}
157 | {n =>

{n.info.age}

}
158 |

{isWatchUser && 'OK'}

159 | 160 |
161 |

Array Case

162 |
163 | 166 | 169 | 172 | 175 | 178 | 181 | 184 | 187 |
188 | 189 |

{arrayPick}

190 |

{sLs.value[0].count}

191 |

{ls[0].count}

192 | {n =>

{n[0].count}

}
193 | {n =>

{n[0].count}

}
194 |

{isWatchLs && 'OK'}

195 | 196 | ); 197 | } 198 | -------------------------------------------------------------------------------- /src/package/signify-core/index.ts: -------------------------------------------------------------------------------- 1 | import { syncSystem } from '../signify-sync'; 2 | import { cacheUpdateValue, getInitialValue } from '../signify-cache'; 3 | import { deepClone } from '../utils/objectClone'; 4 | import { deepCompare } from '../utils/objectCompare'; 5 | import { HardWrapCore, WrapCore, htmlCore, subscribeCore, useCore, watchCore } from './signify.core'; 6 | import { TConditionRendering, TConditionUpdate as TConditionUpdating, TListeners, TOmitHtml, TSetterCallback, TSignifyConfig, TUseValueCb } from './signify.model'; 7 | 8 | /** 9 | * Signify class for managing a reactive state in a React environment. 10 | * 11 | * This class encapsulates the state management logic, providing features such 12 | * as synchronization with external systems, conditional updates and rendering, 13 | * and various utilities to interact with the state. 14 | * 15 | * @template T - Type of the state value. 16 | */ 17 | class Signify { 18 | #isRender = true; // Indicates whether the component should re-render. 19 | #initialValue: T; // Stores the initial value of the state. 20 | #value: T; // Current value of the state. 21 | #config?: TSignifyConfig; // Configuration options for Signify. 22 | #listeners: TListeners = new Set(); // Listeners for state changes. 23 | #coreListeners: TListeners = new Set(); // Listeners for state changes which check every change of state1. 24 | #syncSetter?: TUseValueCb; // Function to synchronize state externally. 25 | #conditionUpdating?: TConditionUpdating; // Condition for updating the state. 26 | #conditionRendering?: TConditionRendering; // Condition for rendering. 27 | 28 | /** 29 | * Constructor to initialize the Signify instance with an initial value and optional configuration. 30 | * 31 | * @param initialValue - The initial value of the state. 32 | * @param config - Optional configuration settings for state management. 33 | */ 34 | constructor(initialValue: T, config?: TSignifyConfig) { 35 | this.#initialValue = initialValue; // set initial value. 36 | this.#value = getInitialValue(deepClone(initialValue), config?.cache); // Get initial value considering caching. 37 | this.#config = config; 38 | 39 | // If synchronization is enabled, setup the sync system. 40 | if (config?.syncKey) { 41 | const { post, sync } = syncSystem({ 42 | key: config.syncKey, 43 | cb: data => { 44 | this.#value = data; // Update local state with synchronized value. 45 | cacheUpdateValue(this.value, this.#config?.cache); // Update cache with new value. 46 | this.#inform(); // Notify listeners about the state change. 47 | } 48 | }); 49 | 50 | this.#syncSetter = post; // Assign the sync setter function. 51 | 52 | sync(() => this.value); // Sync on value changes. 53 | } 54 | } 55 | 56 | /** 57 | * Inform all listeners about the current value if rendering is allowed. 58 | */ 59 | #inform = (isEnableCore = true) => { 60 | if (this.#isRender && (!this.#conditionRendering || this.#conditionRendering(this.value))) { 61 | this.#listeners.forEach(listener => listener(this.value)); // Notify each listener with the current value. 62 | } 63 | 64 | isEnableCore && this.#coreListeners.forEach(listener => listener(this.value)); 65 | }; 66 | 67 | /** 68 | * Force update the current state and inform listeners of the change. 69 | * 70 | * @param value - New value to set. 71 | */ 72 | #forceUpdate = (value?: T) => { 73 | if (value !== undefined) { 74 | this.#value = value; // Update current value. 75 | } 76 | cacheUpdateValue(this.value, this.#config?.cache); // Update cache if applicable. 77 | this.#syncSetter?.(this.value); // Synchronize with external system if applicable. 78 | this.#inform(); // Notify listeners about the new value. 79 | }; 80 | 81 | /** 82 | * Getter for obtaining the current value of the state. 83 | */ 84 | get value(): T { 85 | return this.#value; 86 | } 87 | 88 | /** 89 | * Setter function to update the state. Can take a new value or a callback function which use to update value directly. 90 | * 91 | * @param v - New value or a callback to compute the new value based on current state. 92 | */ 93 | readonly set = (v: T | TSetterCallback, isForceUpdate = false) => { 94 | let tempVal: T; 95 | 96 | if (typeof v === 'function') { 97 | let params = { value: isForceUpdate ? this.#value : deepClone(this.#value) }; 98 | (v as TSetterCallback)(params); // Determine new value. 99 | tempVal = params.value; 100 | } else { 101 | tempVal = v; // Determine new value. 102 | } 103 | 104 | // Check if the new value is different and meets update conditions before updating. 105 | if (isForceUpdate || (!deepCompare(this.value, tempVal) && (!this.#conditionUpdating || this.#conditionUpdating(this.value, tempVal)))) { 106 | this.#forceUpdate(tempVal); // Perform forced update if conditions are satisfied. 107 | } 108 | }; 109 | 110 | /** 111 | * Stops rendering updates for this instance. 112 | */ 113 | readonly stop = () => { 114 | this.#isRender = false; // Disable rendering updates. 115 | }; 116 | 117 | /** 118 | * Resumes rendering updates for this instance. 119 | */ 120 | readonly resume = () => { 121 | this.#isRender = true; // Enable rendering updates. 122 | this.#inform(false); // Notify listeners of any current value changes. 123 | }; 124 | 125 | /** 126 | * Resets the state back to its initial value. 127 | */ 128 | readonly reset = () => { 129 | this.#forceUpdate(deepClone(this.#initialValue)); // Reset to initial value. 130 | }; 131 | 132 | /** 133 | * Sets a condition for updating the state. The callback receives previous and new values and returns a boolean indicating whether to update. 134 | * 135 | * @param cb - Callback function for determining update conditions. 136 | */ 137 | readonly conditionUpdating = (cb: TConditionUpdating) => (this.#conditionUpdating = cb); 138 | 139 | /** 140 | * Sets a condition for rendering. The callback receives the current value and returns a boolean indicating whether to render. 141 | * 142 | * @param cb - Callback function for determining render conditions. 143 | */ 144 | readonly conditionRendering = (cb: TConditionRendering) => (this.#conditionRendering = cb); 145 | 146 | /** 147 | * Function to use the current value in components. This provides reactivity to component updates based on state changes. 148 | */ 149 | readonly use = useCore(this.#listeners, () => this.value); 150 | 151 | /** 152 | * Function to watch changes on state and notify listeners accordingly. 153 | */ 154 | readonly watch = watchCore(this.#coreListeners); 155 | 156 | /** 157 | * Function to subscribe to state changes and notify listeners accordingly. 158 | */ 159 | readonly subscribe = subscribeCore(this.#coreListeners); 160 | 161 | /** 162 | * Generates HTML output from the use function to render dynamic content based on current state. 163 | */ 164 | readonly html = htmlCore(this.use); 165 | 166 | /** 167 | * A wrapper component that allows for rendering based on current state while managing reactivity efficiently. 168 | */ 169 | readonly Wrap = WrapCore(this.use); 170 | 171 | /** 172 | * A hard wrapper component that provides additional control over rendering and avoids unnecessary re-renders in parent components. 173 | */ 174 | readonly HardWrap = HardWrapCore(this.use); 175 | 176 | /** 177 | * Creates a sliced version of the state by applying a function to derive a part of the current value. 178 | * 179 | * @param pick - Function that extracts a portion of the current value. 180 | */ 181 | readonly slice =

(pick: (v: T) => P) => { 182 | let _value: P = pick(this.value), // Extracted portion of the current state. 183 | _isRender = true, // Flag to manage rendering for sliced values. 184 | _conditionRendering: TConditionRendering

| undefined; // Condition for rendering sliced values. 185 | const _listeners: TListeners

= new Set(), // Listeners for sliced values. 186 | _coreListeners: TListeners

= new Set(), 187 | _inform = (isEnableCore = true) => { 188 | const temp = pick(this.value); // Get new extracted portion of the state. 189 | 190 | if (_isRender && (!_conditionRendering || _conditionRendering(temp))) { 191 | _value = temp; // Update sliced value if conditions are met. 192 | _listeners.forEach(listener => listener(temp)); // Notify listeners of sliced value change. 193 | } 194 | 195 | isEnableCore && _coreListeners.forEach(listener => listener(temp)); 196 | }, 197 | use = useCore(_listeners, () => _value), // Core function for reactivity with sliced values. 198 | control = { 199 | value: _value, 200 | use, 201 | watch: watchCore(_coreListeners), // Watch changes for sliced values. 202 | html: htmlCore(use), // Generate HTML output for sliced values. 203 | Wrap: WrapCore(use), // Wrapper component for sliced values with reactivity. 204 | HardWrap: HardWrapCore(use), // Hard wrapper component for more control over rendering of sliced values. 205 | stop: () => (_isRender = false), // Stop rendering updates for sliced values. 206 | resume: () => { 207 | _isRender = true; // Resume rendering updates for sliced values. 208 | _inform(false); // Inform listeners about any changes after resuming. 209 | }, 210 | conditionRendering: (cb: TConditionRendering

) => (_conditionRendering = cb), // Set condition for rendering sliced values. 211 | subscribe: subscribeCore(_coreListeners) // Subscribe to state changes for sliced values. 212 | }; 213 | 214 | // Add a listener to inform when the original state changes affecting the sliced output. 215 | this.#listeners.add(() => { 216 | if (!deepCompare(pick(this.value), _value)) { 217 | _inform(); // Trigger inform if sliced output has changed due to original state change. 218 | } 219 | }); 220 | 221 | Object.defineProperty(control, 'value', { 222 | get: () => _value, // Getter for accessing sliced value directly. 223 | enumerable: false, 224 | configurable: false 225 | }); 226 | 227 | return control as TOmitHtml; // Return control object without HTML methods exposed directly. 228 | }; 229 | } 230 | 231 | /** 232 | * ReactSignify 233 | * - 234 | * @link https://reactsignify.dev 235 | * @description 236 | * Factory function to create a new Signify instance with an initial value and optional configuration settings. 237 | * 238 | * @template T - Type of the initial state value. 239 | * @param initialValue - The initial value to start with in Signify instance. 240 | * @param config - Optional configuration settings for Signify instance behavior. 241 | * 242 | * @returns A new instance of Signify configured with provided initial settings. 243 | */ 244 | export const signify = (initialValue: T, config?: TSignifyConfig): TOmitHtml> => new Signify(initialValue, config); 245 | --------------------------------------------------------------------------------