{"id":13447540,"url":"https://github.com/sindresorhus/on-change","last_synced_at":"2026-02-01T17:00:32.482Z","repository":{"id":41390259,"uuid":"113883812","full_name":"sindresorhus/on-change","owner":"sindresorhus","description":"Watch an object or array for changes","archived":false,"fork":false,"pushed_at":"2025-10-27T02:50:59.000Z","size":200,"stargazers_count":2029,"open_issues_count":3,"forks_count":90,"subscribers_count":18,"default_branch":"main","last_synced_at":"2026-01-23T22:38:45.516Z","etag":null,"topics":["change-detection","es2015-proxy","nodejs","npm-package","object","watch"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sindresorhus.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":"license","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/security.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":"sindresorhus","open_collective":"sindresorhus","buy_me_a_coffee":"sindresorhus","custom":"https://sindresorhus.com/donate"}},"created_at":"2017-12-11T16:45:21.000Z","updated_at":"2026-01-20T16:47:37.000Z","dependencies_parsed_at":"2024-11-05T19:27:06.685Z","dependency_job_id":"62aaaa34-8d53-41e6-838b-7fa13bd20cdd","html_url":"https://github.com/sindresorhus/on-change","commit_stats":{"total_commits":123,"total_committers":21,"mean_commits":5.857142857142857,"dds":0.4390243902439024,"last_synced_commit":"a9cc1e2a1b990478752f8a58a483bf319dd7672a"},"previous_names":[],"tags_count":37,"template":false,"template_full_name":null,"purl":"pkg:github/sindresorhus/on-change","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sindresorhus%2Fon-change","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sindresorhus%2Fon-change/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sindresorhus%2Fon-change/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sindresorhus%2Fon-change/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sindresorhus","download_url":"https://codeload.github.com/sindresorhus/on-change/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sindresorhus%2Fon-change/sbom","scorecard":{"id":826646,"data":{"date":"2025-08-11","repo":{"name":"github.com/sindresorhus/on-change","commit":"3139e12300245ec36507913ce4193253505f33c3"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":4.2,"checks":[{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Security-Policy","score":10,"reason":"security policy file detected","details":["Info: security policy file detected: .github/security.md:1","Info: Found linked content: .github/security.md:1","Info: Found disclosure, vulnerability, and/or timelines in security policy: .github/security.md:1","Info: Found text in security policy: .github/security.md:1"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Code-Review","score":2,"reason":"Found 6/30 approved changesets -- score normalized to 2","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/main.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/main.yml:16: update your workflow using https://app.stepsecurity.io/secureworkflow/sindresorhus/on-change/main.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/main.yml:17: update your workflow using https://app.stepsecurity.io/secureworkflow/sindresorhus/on-change/main.yml/main?enable=pin","Warn: npmCommand not pinned by hash: .github/workflows/main.yml:21","Info:   0 out of   2 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   1 npmCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: license:0","Info: FSF or OSI recognized license: MIT License: license:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 6 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-23T16:45:28.181Z","repository_id":41390259,"created_at":"2025-08-23T16:45:28.182Z","updated_at":"2025-08-23T16:45:28.182Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28983423,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-01T16:29:42.054Z","status":"ssl_error","status_checked_at":"2026-02-01T16:29:41.428Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["change-detection","es2015-proxy","nodejs","npm-package","object","watch"],"created_at":"2024-07-31T05:01:20.465Z","updated_at":"2026-02-01T17:00:32.413Z","avatar_url":"https://github.com/sindresorhus.png","language":"JavaScript","funding_links":["https://github.com/sponsors/sindresorhus","https://opencollective.com/sindresorhus","https://buymeacoffee.com/sindresorhus","https://sindresorhus.com/donate"],"categories":["JavaScript","DevTools","Utilities"],"sub_categories":["Languages","React Components"],"readme":"# on-change\n\n\u003e Watch an object or array for changes\n\nIt works recursively, so it will even detect if you modify a deep property like `obj.a.b[0].c = true`.\n\nUses the [`Proxy` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy).\n\n## Install\n\n```sh\nnpm install on-change\n```\n\n## Usage\n\n```js\nimport onChange from 'on-change';\n\nconst object = {\n\tfoo: false,\n\ta: {\n\t\tb: [\n\t\t\t{\n\t\t\t\tc: false\n\t\t\t}\n\t\t]\n\t}\n};\n\nlet index = 0;\nconst watchedObject = onChange(object, function (path, value, previousValue, applyData) {\n\tconsole.log('Object changed:', ++index);\n\tconsole.log('this:', this);\n\tconsole.log('path:', path);\n\tconsole.log('value:', value);\n\tconsole.log('previousValue:', previousValue);\n\tconsole.log('applyData:', applyData);\n});\n\nwatchedObject.foo = true;\n//=\u003e 'Object changed: 1'\n//=\u003e 'this: {\n//   \tfoo: true,\n//   \ta: {\n//   \t\tb: [\n//   \t\t\t{\n//   \t\t\t\tc: false\n//   \t\t\t}\n//   \t\t]\n//   \t}\n//   }'\n//=\u003e 'path: \"foo\"'\n//=\u003e 'value: true'\n//=\u003e 'previousValue: false'\n//=\u003e 'applyData: undefined'\n\nwatchedObject.a.b[0].c = true;\n//=\u003e 'Object changed: 2'\n//=\u003e 'this: {\n//   \tfoo: true,\n//   \ta: {\n//   \t\tb: [\n//   \t\t\t{\n//   \t\t\t\tc: true\n//   \t\t\t}\n//   \t\t]\n//   \t}\n//   }'\n//=\u003e 'path: \"a.b.0.c\"'\n//=\u003e 'value: true'\n//=\u003e 'previousValue: false'\n//=\u003e 'applyData: undefined'\n\nwatchedObject.a.b.push(3);\n//=\u003e 'Object changed: 3'\n//=\u003e 'this: {\n//   \tfoo: true,\n//   \ta: {\n//   \t\tb: [\n//   \t\t\t{\n//   \t\t\t\tc: true\n//   \t\t\t},\n//   \t\t\t3\n//   \t\t]\n//   \t}\n//   }'\n//=\u003e 'path: \"a.b\"'\n//=\u003e 'value: [{c: true}, 3]'\n//=\u003e 'previousValue: [{c: true}]'\n//=\u003e 'applyData: {\n//       name: \"push\",\n//       args: [3],\n//       result: 2,\n//   }'\n\n// Access the original object\nonChange.target(watchedObject).foo = false;\n// Callback isn't called\n\n// Unsubscribe\nonChange.unsubscribe(watchedObject);\nwatchedObject.foo = 'bar';\n// Callback isn't called\n```\n\n## API\n\n### onChange(object, onChange, options?)\n\nReturns a version of `object` that is watched. It's the exact same object, just with some `Proxy` traps.\n\n#### object\n\nType: `object`\n\nObject to watch for changes.\n\n#### onChange\n\nType: `Function`\n\nFunction that gets called anytime the object changes.\n\nThe function receives four arguments:\n1. A path to the value that was changed. A change to `c` in the above example would return `a.b.0.c`.\n2. The new value at the path.\n3. The previous value at the path. Changes in `WeakSets` and `WeakMaps` will return `undefined`.\n4. An object with the name of the method that produced the change, the args passed to the method, and the result of the method.\n\nThe context (this) is set to the original object passed to `onChange` (with Proxy).\n\n#### options\n\nType: `object`\n\nOptions for altering the behavior of onChange.\n\n##### isShallow\n\nType: `boolean`\\\nDefault: `false`\n\nDeep changes will not trigger the callback. Only changes to the immediate properties of the original object.\n\n##### equals\n\nType: `Function`\\\nDefault: [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)\n\nThe function receives two arguments to be compared for equality. Should return `true` if the two values are determined to be equal. Useful if you only need a more loose form of equality.\n\n##### ignoreSymbols\n\nType: `boolean`\\\nDefault: `false`\n\nSetting properties as `Symbol` won't trigger the callback.\n\n##### ignoreKeys\n\nType: `Array\u003cstring | symbol\u003e`\\\nDefault: `undefined`\n\nSetting properties in this array won't trigger the callback.\n\n##### ignoreUnderscores\n\nType: `boolean`\\\nDefault: `false`\n\nSetting properties with an underscore as the first character won't trigger the callback.\n\n##### pathAsArray\n\nType: `boolean`\\\nDefault: `false`\n\nThe path will be provided as an array of keys instead of a delimited string. Recommended when working with Sets, Maps, or property keys that are Symbols.\n\n##### ignoreDetached\n\nType: `boolean`\\\nDefault: `false`\n\nIgnore changes to objects that become detached from the watched object.\n\n##### details\n\nType: `boolean|string[]`\\\nDefault: `false`\n\nTrigger callbacks for each change within specified method calls or all method calls.\nWhen enabled, mutating array and collection methods will emit per-item paths (for example `selectedFeatures.0`) instead of the collection path. Use `details: ['forEach']` to expand only specific methods.\n\n##### onValidate\n\nType: `Function`\n\nThe function receives the same arguments and context as the [onChange callback](#onchange). The function is called whenever a change is attempted. Returning true will allow the change to be made and the onChange callback to execute, returning anything else will prevent the change from being made and the onChange callback will not trigger.\n\n### onChange.target(object)\n\nReturns the original unwatched object.\n\n#### object\n\nType: `object`\n\nObject that is already being watched for changes.\n\n### onChange.unsubscribe(object)\n\nCancels all future callbacks on a watched object and returns the original unwatched object.\n\n#### object\n\nType: `object`\n\nObject that is already being watched for changes.\n\n## Use-case\n\nI had some code that was like:\n\n```js\nconst foo = {\n\ta: 0,\n\tb: 0\n};\n\n// …\n\nfoo.a = 3;\nsave(foo);\n\n// …\n\nfoo.b = 7;\nsave(foo);\n\n\n// …\n\nfoo.a = 10;\nsave(foo);\n```\n\nNow it can be simplified to:\n\n```js\nconst foo = onChange({\n\ta: 0,\n\tb: 0\n}, () =\u003e save(foo));\n\n// …\n\nfoo.a = 3;\n\n// …\n\nfoo.b = 7;\n\n// …\n\nfoo.a = 10;\n```\n\n## Related\n\n- [known](https://github.com/sindresorhus/known) - Allow only access to known object properties *(Uses `Proxy` too)*\n- [negative-array](https://github.com/sindresorhus/negative-array) - Negative array index support `array[-1]` *(Uses `Proxy` too)*\n- [atama](https://github.com/franciscop/atama) - State manager *(Uses `Proxy` too)*\n- [introspected](https://github.com/WebReflection/introspected) - Never-ending Proxy with multiple observers *(Uses `Proxy` too)*\n\n## Maintainers\n\n- [Sindre Sorhus](https://github.com/sindresorhus)\n- [Darren Wright](https://github.com/DarrenPaulWright)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsindresorhus%2Fon-change","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsindresorhus%2Fon-change","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsindresorhus%2Fon-change/lists"}