{"id":32264516,"url":"https://github.com/jesseskinner/hover","last_synced_at":"2026-02-20T20:03:15.326Z","repository":{"id":25326247,"uuid":"28753283","full_name":"jesseskinner/hover","owner":"jesseskinner","description":"A very lightweight data store with action reducers and state change listeners.","archived":false,"fork":false,"pushed_at":"2016-08-20T21:30:01.000Z","size":169,"stargazers_count":99,"open_issues_count":3,"forks_count":4,"subscribers_count":5,"default_branch":"master","last_synced_at":"2026-01-12T15:33:05.113Z","etag":null,"topics":["flux","flux-architecture","javascript","react","reactjs","redux"],"latest_commit_sha":null,"homepage":"","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/jesseskinner.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":null,"support":null}},"created_at":"2015-01-03T20:02:03.000Z","updated_at":"2025-02-06T00:42:54.000Z","dependencies_parsed_at":"2022-08-25T03:41:16.309Z","dependency_job_id":null,"html_url":"https://github.com/jesseskinner/hover","commit_stats":null,"previous_names":["jesseskinner/hoverboard"],"tags_count":54,"template":false,"template_full_name":null,"purl":"pkg:github/jesseskinner/hover","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jesseskinner%2Fhover","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jesseskinner%2Fhover/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jesseskinner%2Fhover/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jesseskinner%2Fhover/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jesseskinner","download_url":"https://codeload.github.com/jesseskinner/hover/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jesseskinner%2Fhover/sbom","scorecard":{"id":516557,"data":{"date":"2025-08-11","repo":{"name":"github.com/jesseskinner/hover","commit":"b2f024eb73f9f3a32a47856321f553c3dec61afb"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3,"checks":[{"name":"Code-Review","score":0,"reason":"Found 1/28 approved changesets -- score normalized to 0","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":"Dangerous-Workflow","score":-1,"reason":"no workflows found","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":"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":"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":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"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":"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":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"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":"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":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"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":"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":"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":"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 'master'"],"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 3 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-20T01:58:16.848Z","repository_id":25326247,"created_at":"2025-08-20T01:58:16.848Z","updated_at":"2025-08-20T01:58:16.848Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29662585,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-20T19:49:36.704Z","status":"ssl_error","status_checked_at":"2026-02-20T19:44:05.372Z","response_time":59,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: 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":["flux","flux-architecture","javascript","react","reactjs","redux"],"created_at":"2025-10-22T21:02:50.197Z","updated_at":"2026-02-20T20:03:15.316Z","avatar_url":"https://github.com/jesseskinner.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n    \u003cimg height=\"300\" width=\"300\" src=\"https://tfotw.com/hover-300px.png\" title=\"Hover\"\u003e\u003cbr\u003e\n\tA very lightweight data store\u003cbr\u003e\n\twith action reducers\u003cbr\u003e\n\tand state change listeners.\u003cbr\u003e\n\t\u003cbr\u003e\n\t\u003cbr\u003e\n\u003c/p\u003e\n\n[![NPM version][npm-image]][npm-url] [![Downloads][downloads-image]][npm-url]\n[![Build Status][travis-image]][travis-url] [![Coverage Status][coveralls-image]][coveralls-url]\n[![Dependency status][david-dm-image]][david-dm-url] [![Dev Dependency status][david-dm-dev-image]][david-dm-dev-url]\n\n## Installation\n\nYou can use npm to install Hover, or [download the raw file here](https://raw.githubusercontent.com/jesseskinner/hover/master/src/index.js).\n\nFor more information, check out the [Concept](#concept), [Usage](#usage), [Documentation](#documentation) and [FAQ](#faq) below.\n\n\n```\nnpm install hover\n\nimport Hover from 'hover'\n```\n\n## Concept\n\nHover is a place to keep your state. You can pass in an initial state, but the only way to change the state afterwards is through action reducers you define.\n\nThe basic usage of Hover is:\n\n```javascript\n// in store.js\nimport Hover from 'hover'\n\nconst actions = {\n\tincrement: (state, amount) =\u003e state + amount\n}\n\nconst initialState = 0\n\nexport default new Hover(actions, initialState)\n\n// elsewhere\nimport store from './store'\nconst state = store.increment(2)\n```\n\nYou can easily subscribe to state changes with Hover stores. You can pass a callback to the store. Your callback will be called immediately at first, and again whenever the state changes. Here's an example using vanilla DOM scripting to update the page:\n\n```javascript\nfunction renderUserProfile (user) {\n\tif (user) {\n\t\tconst root = document.getElementById('user-profile'),\n\t\t\tavatar = root.querySelector('.avatar'),\n\t\t\tname = root.querySelector('.name')\n\n\t\t// use the avatar url as an image source\n\t\tavatar.src = user.avatar\n\n\t\t// erase previous contents of name\n\t\twhile (name.firstChild) {\n\t\t\tname.removeChild(name.firstChild)\n\t\t}\n\n\t\t// add name as a text node\n\t\tname.appendChild(document.createTextNode(user.name))\n\t}\n}\n\nuserStore(renderUserProfile)\n```\n\nHere's an example rendering a React component:\n\n```jsx\nfunction renderUserProfile (user) {\n\tReactDOM.render(\n\t\t\u003cUserProfile user={user} actions={userStore} /\u003e,\n\t\tdocument.getElementById('user-profile')\n\t)\n}\n\nuserStore(renderUserProfile)\n```\n\n\n## Usage\n\nHere's how you might use Hover to keep track of clicks with a clickCounter.\n\n```javascript\nconst actions = {\n\tclick: (state, text) =\u003e ({\n\t\tvalue: state.value + 1,\n\t\tlog: state.log.concat(text)\n\t}),\n\n\t// go back to defaults\n\treset: () =\u003e initialState\n}\n\nconst initialState = 0\n\nconst clickCounter = new Hover(actions, initialState)\n\n// listen to changes to the state\nconst unsubscribe = clickCounter(clickState =\u003e\n\tdocument.write(JSON.stringify(clickState) + \"\u003cbr\u003e\"\n)\n\nclickCounter.click('first')\nclickCounter.click('second')\n\n// reset back to zero\nclickCounter.reset()\n\nunsubscribe()\n\nclickCounter.click(\"This won't show up\")\n```\n\nIf you run this example, you'll see this:\n\n```javascript\n{\"value\":0,\"log\":[]}\n{\"value\":1,\"log\":[\"first\"]}\n{\"value\":2,\"log\":[\"first\",\"second\"]}\n{\"value\":0,\"log\":[]}\n```\n\nTo see how Hover can fit into a larger app, with React and a router, check out the [Hover TodoMVC](http://github.com/jesseskinner/hover-todomvc/).\n\n\n## Documentation\n\nHover is a function that takes an actions object and returns a store object.\n\n### Syntax\n\n```javascript\nstore = new Hover(actions[, initialState])\n```\n\n#### `actions` object\n\n- Any properties of the actions object will be exposed as methods on the returned `store` object.\n- If your state is a plain object, and you return plain objects from your actions, they will be shallow merged together.\n- Note that your actions will automatically receive `state` as the first parameter, followed by the arguments you pass in when calling it.\n\n\t```javascript\n\t// store is synchronous, actions are setters\n\tstore = new Hover({\n\t\titems: (state, items) =\u003e ({ items }),\n\t\terror: (state, error) =\u003e ({ error })\n\t}, {})\n\n\t// load data asynchronously and call actions to change the state\n\tapi.getItems((error, items) =\u003e {\n\t\tif (error) {\n\t\t\treturn store.error(error)\n\t\t}\n\n\t\tstore.items(items)\n\t})\n\n\t// listen to the state, and respond to it accordingly\n\tstore(state =\u003e {\n\t\tif (state.items) {\n\t\t\trenderItems(state.items)\n\t\t} else if (state.error) {\n\t\t\talert('Error loading items!')\n\t\t}\n\t})\n\t```\n\n#### Return value\n\n`store = new Hover(actions[, initialState])`\n\n##### `store` object methods\n\n- `store()`\n\n\t- Returns the store's current state.\n\n- `unsubscribe = store(function)`\n\n\t- Adds a listener to the state of a store.\n\n\t- The listener callback will be called immediately, and again whenever the state changed.\n\n    - Returns an unsubscribe function. Call it to stop listening to the state.\n\n\t\t```javascript\n\t\tunsubscribe = store(state =\u003e console.log(state))\n\n        // stop listening\n        unsubscribe()\n\t\t```\n\n- `state = store.action(arg0, arg1, ..., argN)`\n\t- Calls an action handler on the store, passing through any arguments.\n\n\t\t```javascript\n\t\tstore = new Hover({\n\t\t\tadd: (state, number) =\u003e state + number\n\t\t}, 0)\n\n\t\tresult = store() // returns 0\n\t\tresult = store.add(5) // returns 5\n\t\tresult = store.add(4) // returns 9\n\t\tresult = store() // returns 9\n\t\t```\n\n#### Hover.compose\n\n`Hover.compose` takes a definition and creates a store,\nsubscribing to any store members of the definition.\n\n`Hover.compose` can take static variables, objects or arrays.\n\n```javascript\n// create two stores\nconst scoreStore = new Hover({\n    add: (state, score) =\u003e state + score\n}, 0)\nconst healthStore = new Hover({\n    hit: (state, amount) =\u003e state - amount\n}, 100)\n\n// compose the two stores into a single store\nconst gameStore = Hover.compose({\n    score: scoreStore,\n\n    // create an anonymous store to nest objects\n    character: Hover.compose({\n        health: healthStore\n    })\n})\n\n// stores and actions can be accessed with the same structure\ngameStore.score.add(2)\n\ngameStore.character.health.hit(1)\n```\n\nYou can also pass zero or more translate functions after your compose definition,\nto automatically translate or map the state every time it gets updated.\n\nThese translate functions will receive a `state` argument, and must return the resulting state.\n\n```javascript\n// create stores to contain the active and completed todos\nconst activeTodoStore = Hover.compose(todoStore, todos =\u003e\n    todos.filter(todo =\u003e todo.completed === false)\n)\n\nconst completedTodoStore = Hover.compose(todoStore, todos =\u003e\n    todos.filter(todo =\u003e todo.completed === true)\n})\n```\n\n## FAQ\n\n*Q: How does Hover handle asynchronous loading of data from an API?*\n\nThere are three ways to achieve this. One way is to load the API outside of the store, and call actions to pass in the loading state, data and/or error as it arrives:\n\n```javascript\nconst store = new Hover({\n\tloading: (state, isLoading) =\u003e ({ isLoading }),\n\tdata: (state, data) =\u003e ({ data }),\n\terror: (state, error) =\u003e ({ error })\n})\n\nstore.loading(true)\n\ngetDataFromAPI(params, (error, data) =\u003e {\n\tif (error) {\n\t\treturn store.error(error)\n\t}\n\n\tstore.data(data)\n})\n```\n\nAnother way is to make API calls from inside your actions.\n\n```javascript\nconst store = new Hover({\n\tload: (state, params) =\u003e {\n\t\tgetDataFromAPI(params, (error, data) =\u003e\n\t\t\tstore.done(error, data)\n\t\t)\n\n\t\treturn { isLoading: true, error: null, data: null }\n\t},\n\tdone: (state, error, data) =\u003e (\n\t\t{ isLoading: false, error, data }\n\t)\n})\n\nstore.load(params)\n```\n\n---\n\n*Q: If Hover stores only have a single getter, how can I have something like getById?*\n\nIf you have access to a list of items in the state, you can write code to search through the list. You could even have a function like this as a property of the store, before you export it, eg.\n\n```javascript\nimport Hover from 'hover'\n\nconst initialState = [{ id: 1, name: 'one' }, /* etc... */ }\n\nconst itemStore = new Hover({\n\tadd: (list, item) =\u003e list.concat(item)\n}, initialState)\n\n// add a helper function to the store\nitemStore.getById = id =\u003e\n\tlist.filter(item =\u003e item.id === id).pop()\n\n// getAll\nconst items = itemStore()\n\n// look up a specific item\nconst item = itemStore.getById(5)\n\n```\n\n---\n\n\n## Versioning\n\nHover follows [semver versioning](http://semver.org/). So you can be sure that the API won't change until the next major version.\n\n\n## Testing\n\nClone the GitHub repository, run `npm install`, and then run `npm test` to run the tests. Hover has 100% test coverage.\n\n\n## Contributing\n\nFeel free to [fork this repository on GitHub](https://github.com/jesseskinner/hover/fork), make some changes, and make a [Pull Request](https://github.com/jesseskinner/hover/pulls).\n\nYou can also [create an issue](https://github.com/jesseskinner/hover/issues) if you find a bug or want to request a feature.\n\nAny comments and questions are very much welcome as well.\n\n\n## Author\n\nJesse Skinner [@JesseSkinner](http://twitter.com/JesseSkinner)\n\n\n## License\n\nMIT\n\n[coveralls-image]: https://coveralls.io/repos/jesseskinner/hover/badge.png\n[coveralls-url]: https://coveralls.io/r/jesseskinner/hover\n\n[npm-url]: https://npmjs.org/package/hover\n[downloads-image]: http://img.shields.io/npm/dm/hover.svg\n[npm-image]: http://img.shields.io/npm/v/hover.svg\n[travis-url]: https://travis-ci.org/jesseskinner/hover\n[travis-image]: http://img.shields.io/travis/jesseskinner/hover.svg\n[david-dm-url]:https://david-dm.org/jesseskinner/hover\n[david-dm-image]:https://david-dm.org/jesseskinner/hover.svg\n[david-dm-dev-url]:https://david-dm.org/jesseskinner/hover#info=devDependencies\n[david-dm-dev-image]:https://david-dm.org/jesseskinner/hover/dev-status.svg\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjesseskinner%2Fhover","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjesseskinner%2Fhover","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjesseskinner%2Fhover/lists"}