{"id":44530705,"url":"https://github.com/helloitsjoe/react-hooks-compose","last_synced_at":"2026-02-13T18:34:56.799Z","repository":{"id":35041801,"uuid":"199745623","full_name":"helloitsjoe/react-hooks-compose","owner":"helloitsjoe","description":"Decouple Hooks from the presentational components that use them","archived":false,"fork":false,"pushed_at":"2023-01-08T16:04:21.000Z","size":1180,"stargazers_count":174,"open_issues_count":2,"forks_count":7,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-10-16T08:20:58.617Z","etag":null,"topics":["react","react-hooks","reactjs"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/helloitsjoe.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-07-31T00:08:29.000Z","updated_at":"2025-10-14T20:57:54.000Z","dependencies_parsed_at":"2023-01-15T12:36:19.129Z","dependency_job_id":null,"html_url":"https://github.com/helloitsjoe/react-hooks-compose","commit_stats":null,"previous_names":[],"tags_count":30,"template":false,"template_full_name":null,"purl":"pkg:github/helloitsjoe/react-hooks-compose","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/helloitsjoe%2Freact-hooks-compose","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/helloitsjoe%2Freact-hooks-compose/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/helloitsjoe%2Freact-hooks-compose/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/helloitsjoe%2Freact-hooks-compose/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/helloitsjoe","download_url":"https://codeload.github.com/helloitsjoe/react-hooks-compose/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/helloitsjoe%2Freact-hooks-compose/sbom","scorecard":{"id":460380,"data":{"date":"2025-08-11","repo":{"name":"github.com/helloitsjoe/react-hooks-compose","commit":"be0a459400769d7b30b17c41595721ade949eebb"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.4,"checks":[{"name":"Code-Review","score":0,"reason":"Found 0/12 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":"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":"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":"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":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/publish.yml:1","Warn: no topLevel permission defined: .github/workflows/verify.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":"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/publish.yml:10: update your workflow using https://app.stepsecurity.io/secureworkflow/helloitsjoe/react-hooks-compose/publish.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish.yml:11: update your workflow using https://app.stepsecurity.io/secureworkflow/helloitsjoe/react-hooks-compose/publish.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/verify.yml:14: update your workflow using https://app.stepsecurity.io/secureworkflow/helloitsjoe/react-hooks-compose/verify.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/verify.yml:15: update your workflow using https://app.stepsecurity.io/secureworkflow/helloitsjoe/react-hooks-compose/verify.yml/main?enable=pin","Info:   0 out of   4 GitHub-owned GitHubAction 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":"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":"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":0,"reason":"license file not detected","details":["Warn: project does not have a license file"],"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":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"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 24 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"}},{"name":"Vulnerabilities","score":0,"reason":"23 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-968p-4wvh-cqc8","Warn: Project is vulnerable to: GHSA-67hx-6x53-jw92","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-x9w5-v3q2-3rhw","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-434g-2637-qmqr","Warn: Project is vulnerable to: GHSA-49q7-c7j4-3p7m","Warn: Project is vulnerable to: GHSA-977x-g7h5-7qgw","Warn: Project is vulnerable to: GHSA-f7q4-pwc6-w24p","Warn: Project is vulnerable to: GHSA-fc9h-whq2-v747","Warn: Project is vulnerable to: GHSA-vjh7-7g9h-fjfh","Warn: Project is vulnerable to: GHSA-fjxv-7rqg-78g4","Warn: Project is vulnerable to: GHSA-9c47-m6qq-7p4h","Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv","Warn: Project is vulnerable to: GHSA-h7cp-r72f-jxh6","Warn: Project is vulnerable to: GHSA-v62p-rq8g-8h59","Warn: Project is vulnerable to: GHSA-p8p7-x288-28g6","Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw","Warn: Project is vulnerable to: GHSA-52f5-9888-hmc6","Warn: Project is vulnerable to: GHSA-72xf-g2v4-qvf3","Warn: Project is vulnerable to: GHSA-j8xg-fqg3-53r7","Warn: Project is vulnerable to: GHSA-3h5v-q93c-6h6q"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-19T10:58:34.640Z","repository_id":35041801,"created_at":"2025-08-19T10:58:34.640Z","updated_at":"2025-08-19T10:58:34.640Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29414279,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-13T06:24:03.484Z","status":"ssl_error","status_checked_at":"2026-02-13T06:23:12.830Z","response_time":78,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5: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":["react","react-hooks","reactjs"],"created_at":"2026-02-13T18:34:56.584Z","updated_at":"2026-02-13T18:34:56.792Z","avatar_url":"https://github.com/helloitsjoe.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# react-hooks-compose\n\n[![Build Status](https://travis-ci.com/helloitsjoe/react-hooks-compose.svg?branch=master)](https://travis-ci.com/helloitsjoe/react-hooks-compose)\n[![Coverage Status](https://coveralls.io/repos/github/helloitsjoe/react-hooks-compose/badge.svg?branch=master)](https://coveralls.io/github/helloitsjoe/react-hooks-compose?branch=master)\n[![NPM Version](https://img.shields.io/npm/v/react-hooks-compose?color=lightgray)](https://www.npmjs.com/package/react-hooks-compose)\n\n## Installation\n\n```\nnpm i react-hooks-compose\n```\n\n## Why `react-hooks-compose`?\n\n`react-hooks-compose` provides an ergonomic way to decouple hooks from the components that use them.\n\nReact Hooks are great. They encapsulate state logic and make it more reusable. But what if you have\npure presentational components that you want to use with different state? What if you want to test\nyour presentaional component in isolation?\n\nReact Hooks invert the Container/Presenter pattern, putting the container inside the presenter. This\nmakes it hard to use the same presentational component with different hooks, and clunky to test\npresentational components by themselves.\n\nOne option:\n\n```jsx\nimport { Presenter } from './presenter';\nimport { useCustomHook } from './hooks';\n\nconst Wrapper = () =\u003e {\n  const { foo, bar } = useCustomHook();\n  return \u003cPresenter foo={foo} bar={bar} /\u003e;\n};\n\nexport default Wrapper;\n```\n\nThis works fine, but you end up with an extra component just to connect the hook to the Presenter.\nIf you want to test the presenter in isolation, you have to export it separately. there must be a\nbetter way!\n\n## Basic Usage\n\n`composeHooks` passes values from hooks as props, and allows you to pass any other props as normal.\nThis allows you to export the hook, stateful component, and purely presentational component\nseparately.\n\n```jsx\nimport composeHooks from 'react-hooks-compose';\n\nconst useForm = () =\u003e {\n  const [name, setName] = useState('');\n  const onChange = e =\u003e setName(e.target.value);\n  return { name, onChange };\n};\n\n// Other props (in this case `icon`) can be passed in separately\nconst FormPresenter = ({ name, onChange, icon }) =\u003e (\n  \u003cdiv className=\"App\"\u003e\n    \u003cdiv\u003e{icon}\u003c/div\u003e\n    \u003ch1\u003eHello, {name}!\u003c/h1\u003e\n    \u003cinput value={name} onChange={onChange} /\u003e\n  \u003c/div\u003e\n);\n\nexport default composeHooks({ useForm })(FormPresenter);\n```\n\nYou can think of `composeHooks` like `react-redux`'s `connect` HOC. For one thing, it creates an\nimplicit container. You can think of the object passed into `composeHooks` as `mapHooksToProps`,\nsimilar to\n[the object form of `mapDispatchToProps`](https://daveceddia.com/redux-mapdispatchtoprops-object-form/).\n\n### Compose multiple hooks:\n\n```js\nconst Presenter = ({ name, onChange, foo, bar, value }) =\u003e (\n  \u003cdiv className=\"App\"\u003e\n    \u003ch1\u003eHello, {name}!\u003c/h1\u003e\n    \u003cp\u003eContext value is {value}\u003c/p\u003e\n    \u003cp\u003e\n      foo is {foo}, bar is {bar}\n    \u003c/p\u003e\n    \u003cinput value={name} onChange={onChange} /\u003e\n  \u003c/div\u003e\n);\n\nexport default composeHooks({\n  useForm,\n  useFooBar,\n  value: () =\u003e useContext(MyContext), // Usage with `useContext`\n})(FormPresenter);\n```\n\n### Usage with `useState`\n\nIf you compose with `useState` directly (i.e. the prop is an array), the prop will remain an array\nand should be destructured before use:\n\n```jsx\nconst FormPresenter = ({ nameState: [name, setName] }) =\u003e (\n  \u003cdiv className=\"App\"\u003e\n    \u003ch1\u003eHello, {name}!\u003c/h1\u003e\n    \u003cinput value={name} onChange={e =\u003e setName(e.target.value)} /\u003e\n  \u003c/div\u003e\n);\n\nexport default composeHooks({\n  nameState: () =\u003e useState('Calvin'),\n})(FormPresenter);\n```\n\n### Usage with `useEffect`\n\n`useEffect` is supported - the most common usage would be in a custom hook. For example:\n\n```js\nconst usePostData = data =\u003e {\n  const [postStatus, setPostStatus] = useState(SUCCESS);\n\n  useEffect(() =\u003e {\n    setPostStatus(LOADING);\n    postData(data).then(() =\u003e {\n      setPostStatus(SUCCESS);\n    }).catch(err =\u003e {\n      setPostStatus(ERROR);\n    });\n  }, [data]);\n\n  return { postStatus };\n};\n\nconst App = ({ postStatus }) =\u003e { ... };\n\nexport default compose({ usePostData })(App);\n```\n\n### Pass in props for initial values\n\nIf your hooks need access to props to set their initial values, you can pass a function to\n`composeHooks`. This function receives `props` as an argument, and should always return an object:\n\n```jsx\nconst useForm = (initialValue = '') =\u003e {\n  const [value, setValue] = useState(initialValue);\n  const onChange = e =\u003e setValue(e.target.value);\n  return { value, onChange };\n};\n\nconst FormContainer = composeHooks(props =\u003e ({\n  useForm: () =\u003e useForm(props.initialValue),\n}))(FormPresenter);\n\n\u003cFormContainer initialValue=\"Susie\" /\u003e;\n```\n\n## Testing\n\n`composeHooks` is great for testing. Any props you pass in will override the hooks values, so you\ncan test the presenter and container with a single export:\n\n```jsx\n// band-member.js\nconst BandMember = ({singer, onClick}) =\u003e {...} // \u003c-- Presenter\n\nexport default composeHooks({ useName })(BandMember);\n\n// band-member.test.js\nit('returns Joey if singer is true', () =\u003e {\n  // Pass in a `singer` boolean as with any presentational component.\n  // Containers don't usually allow this.\n  const {getByLabelText} = render(\u003cBandMember singer /\u003e);\n  expect(getByLabelText('Name').textContent).toBe('Joey');\n});\n\nit('updates name to Joey when Get Singer button is clicked', () =\u003e {\n  // If you don't pass in props, the component will use the hooks provided\n  // in the module. In this case, `useName` returns `singer` and `onClick`.\n  const {getByLabelText} = render(\u003cBandMember /\u003e);\n  expect(getByLabelText('Name').textContent).toBe('Johnny');\n  fireEvent.click(getByText('Get Singer'));\n  expect(getByLabelText('Name').textContent).toBe('Joey');\n})\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhelloitsjoe%2Freact-hooks-compose","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhelloitsjoe%2Freact-hooks-compose","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhelloitsjoe%2Freact-hooks-compose/lists"}