{"id":18513185,"url":"https://github.com/yeojz/react-unhook","last_synced_at":"2026-04-30T09:32:58.573Z","repository":{"id":57347029,"uuid":"177625003","full_name":"yeojz/react-unhook","owner":"yeojz","description":"React hooks without hooks - a collection of hook-like Null Components","archived":false,"fork":false,"pushed_at":"2024-04-23T01:51:10.000Z","size":1756,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-01-26T13:02:48.323Z","etag":null,"topics":["functional-components","react","react-hooks"],"latest_commit_sha":null,"homepage":"https://yeojz.github.io/react-unhook","language":"TypeScript","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/yeojz.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-03-25T16:34:05.000Z","updated_at":"2019-04-08T15:23:53.000Z","dependencies_parsed_at":"2024-11-06T15:44:16.546Z","dependency_job_id":null,"html_url":"https://github.com/yeojz/react-unhook","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeojz%2Freact-unhook","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeojz%2Freact-unhook/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeojz%2Freact-unhook/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeojz%2Freact-unhook/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yeojz","download_url":"https://codeload.github.com/yeojz/react-unhook/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239225913,"owners_count":19603196,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["functional-components","react","react-hooks"],"created_at":"2024-11-06T15:36:46.963Z","updated_at":"2026-04-30T09:32:53.530Z","avatar_url":"https://github.com/yeojz.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003c!-- markdownlint-disable MD033 --\u003e\n\u003cp align=\"center\"\u003e\u003cimg src=\"./assets/react-unhook-logo.png\" /\u003e\u003c/p\u003e\n\u003c!-- markdownlint-enable MD033 --\u003e\n\n# react-unhook\n\n\u003e React hooks without hooks - a collection of hook-like Null Components\n\n[![npm][npm-badge]][npm-link]\n[![Build Status][circle-badge]][circle-link]\n[![Coverage Status][codecov-badge]][codecov-link]\n[![TypeScript Support][type-ts-badge]][type-ts-link]\n\n\u003c!-- TOC depthFrom:2 --\u003e\n\n- [About](#about)\n- [Motivation](#motivation)\n- [Limitations](#limitations)\n- [Use Case](#use-case)\n- [Usage](#usage)\n- [Example](#example)\n  - [Demo / Storybook](#demo--storybook)\n  - [Effects Without Cleanup](#effects-without-cleanup)\n  - [Effects With Cleanup](#effects-with-cleanup)\n  - [Optimizing Performance](#optimizing-performance)\n- [API Reference](#api-reference)\n  - [Core](#core)\n    - [UseEffect](#useeffect)\n    - [UseCallback](#usecallback)\n  - [Lifecycle](#lifecycle)\n    - [UseEffectOnUpdate](#useeffectonupdate)\n    - [UseEffectOnce](#useeffectonce)\n    - [UseMount](#usemount)\n    - [UseUnmount](#useunmount)\n  - [Timing](#timing)\n    - [UseInterval](#useinterval)\n    - [UseTimeout](#usetimeout)\n  - [Sensors](#sensors)\n    - [UseGeolocation](#usegeolocation)\n  - [UI](#ui)\n    - [UseMouseOut](#usemouseout)\n- [License](#license)\n\n\u003c!-- /TOC --\u003e\n\n## About\n\n`react-unhook` attempts to emulate some of the functionality and segmentation aspect of react hooks,\npackaging it into a standalone \"Null Components\" (components that render `null`) **without** the\nuse of React hooks under-the-hood.\n\n(Note: This is not about avoiding hooks. Just an alternative to some of it).\n\n## Motivation\n\n[React Hooks](https://reactjs.org/docs/hooks-intro.html) are a new addition to React 16.8 and it\nchanges the way we have been approaching React components, formalising new ways of encapsulating logic\nin our components.\n\nTaking inspiration of that, we can make use of the existing lifecycle methods to achive some\nbehaviours of React hooks via Null Components. This allows us to achieve similar code style\nand logic encapsulation of React hooks, aside from low-level / library optimization of hooks.\n\n## Limitations\n\nWith that said, there are some limitations to this Null Component pattern.\n**Stateful hooks (eg: \"useReducer\") cannot be emulated easily** as we are not able to expose\nfunctions of the component without resorting to anti-patterns (eg: using `React.createRef` to\naccess component methods).\n\nHowever, this Null Component pattern **works well for \"listeners\", \"workers\" or\n\"value-change triggers\"** (triggering of a function after a change in value).\nFor example, listening to geolocation changes, interval calls, data fetching on\nparameter changes etc.\n\n## Use Case\n\n```jsx\n// Imagine that you have a signup form that on certain value change,\n// we want to fetch things or asynchronously set values\n// Using \"Null Components\" we can declaratively define those effects.\n\nfunction SignupForm(props) {\n  return (\n    \u003cFragment\u003e\n      \u003cInput name=\"input-one\" /\u003e\n      \u003cInput name=\"input-two\" /\u003e\n      \u003cInput name=\"input-three\" /\u003e\n\n      \u003cFetchWhenInputOneIsFilled name=\"action-one\" /\u003e\n      \u003cValidateWhenTwoIsDirty name=\"action-two\" /\u003e\n      \u003cUpdateInputThreeWhenTwoIsValid name=\"action-three\" /\u003e\n    \u003c/Fragment\u003e\n  );\n}\n```\n\n## Usage\n\n```bash\nnpm install react-unhook --save\n```\n\n```js\nimport { UseCallback, UseEffect } from 'react-unhook';\n```\n\n## Example\n\nThese examples are adopted from React's official docs on hooks.\ni.e. [https://reactjs.org/docs/hooks-effect.html](https://reactjs.org/docs/hooks-effect.html)\n\nThe unhook examples makes use of `withState` HOC ([1][with-state-local], [2][with-state-recompose]) to\nkeep the code style closer to the `hooks` examples. You can also manage your state\nusing a normal class.\n\n### Demo / Storybook\n\nExamples are also available at [http://yeojz.github.io/react-unhook][project-docs-link]\n\n### Effects Without Cleanup\n\nUsing Hooks:\n\n```jsx\nfunction Example() {\n  const [count, setCount] = useState(0);\n\n  useEffect(() =\u003e {\n    document.title = `You clicked ${count} times`;\n  });\n\n  return (\n    \u003cdiv\u003e\n      \u003cp\u003eYou clicked {count} times\u003c/p\u003e\n      \u003cbutton onClick={() =\u003e setCount(count + 1)}\u003eClick me\u003c/button\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\nUsing Unhook:\n\n```jsx\nfunction Example(props) {\n  // assumes you're using withState HOC.\n  // eg: withState('count', 'setCount', 0)(Example);\n  const { count, setCount } = props;\n\n  return (\n    \u003cdiv\u003e\n      \u003cp\u003eYou clicked {count} times\u003c/p\u003e\n      \u003cbutton onClick={() =\u003e setCount(count + 1)}\u003eClick me\u003c/button\u003e\n\n      \u003cUseEffect\n        fn={() =\u003e {\n          document.title = `You clicked ${count} times`;\n        }}\n      /\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n### Effects With Cleanup\n\nUsing Hooks:\n\n```jsx\nfunction FriendStatus(props) {\n  const [isOnline, setIsOnline] = useState(null);\n\n  useEffect(() =\u003e {\n    function handleStatusChange(status) {\n      setIsOnline(status.isOnline);\n    }\n\n    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);\n    // Specify how to clean up after this effect:\n    return function cleanup() {\n      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);\n    };\n  });\n\n  if (isOnline === null) {\n    return 'Loading...';\n  }\n  return isOnline ? 'Online' : 'Offline';\n}\n```\n\nUsing Unhook:\n\n```jsx\nfunction FriendStatus(props) {\n  // withState('isOnline', 'setIsOnline', null)(FriendStatus);\n  const { isOnline, setIsOnline } = props;\n\n  return (\n    \u003cFragment\u003e\n      {isOnline === null ? 'Loading' : isOnline ? 'Online' : 'Offline'}\n\n      \u003cUseEffect\n        fn={() =\u003e {\n          function handleStatusChange(status) {\n            setIsOnline(status.isOnline);\n          }\n\n          ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);\n          // Specify how to clean up after this effect:\n          return function cleanup() {\n            ChatAPI.unsubscribeFromFriendStatus(\n              props.friend.id,\n              handleStatusChange\n            );\n          };\n        }}\n      /\u003e\n    \u003c/Fragment\u003e\n  );\n}\n```\n\n### Optimizing Performance\n\nUsing Hook:\n\n```jsx\nuseEffect(() =\u003e {\n  document.title = `You clicked ${count} times`;\n}, [count]); // Only re-run the effect if count changes\n```\n\nUsing Unhook:\n\n```jsx\n\u003cUseEffect\n  fn={() =\u003e {\n    document.title = `You clicked ${count} times`;\n  }}\n  inputs={[count]}\n/\u003e\n```\n\n## API Reference\n\n**Note:** The comparator function, by default, follows React Hook's `areHookInputsEqual` method,\nwhich uses `Object.is` to compare the values in the array.\n\nAll unhook components make use of `UseCallback` and `UseEffect` at their core.\n\nMany of the components are inspired by hooks from [react-use](https://github.com/streamich/react-use),\nbut re-implmented using react-unhook's `\u003cUseEffect /\u003e` instead of actual React Hooks.\n\n### Core\n\n#### UseEffect\n\nComponent which emulates `useEffect` hook.\n\n```ts\ninterface Props {\n  fn: () =\u003e void | Noop;\n  inputs?: Array\u003cany\u003e;\n  comparator?: EqualityFn;\n}\n```\n\n#### UseCallback\n\nThe difference between `UseEffect` and `UseCallback` is that the function passed\ninto `UseEffect` may return a \"clean-up\" function which will be executed when unmounting\nthe component. In most cases, you can just utilise `UseEffect`.\n\n```ts\ninterface Props {\n  fn: () =\u003e void;\n  inputs?: Array\u003cany\u003e;\n  comparator?: EqualityFn;\n}\n```\n\n### Lifecycle\n\n#### UseEffectOnUpdate\n\nOnly runs the callback when inputs change and not during mounting.\n\n```ts\ninterface Props {\n  fn: () =\u003e void | VoidFn;\n  inputs: any[]; // unlike UseEffect, this is required.\n  comparator?: EqualityFn;\n}\n```\n\n#### UseEffectOnce\n\nAlias method using `UseEffect` with `prop.inputs` preset to `[]`\n\n```ts\ninterface Props {\n  fn: () =\u003e void;\n}\n```\n\n#### UseMount\n\nCalls a function when the component is mounted\n\n```ts\ninterface Props {\n  fn: () =\u003e void;\n}\n```\n\n#### UseUnmount\n\nCalls a function when the component will unmount.\n\n```ts\ninterface Props {\n  fn: () =\u003e void;\n}\n```\n\n### Timing\n\n#### UseInterval\n\nCalls the function at every specified interval (in milliseconds),\neg: Polling.\n\n```ts\ninterface Props {\n  fn: () =\u003e void;\n  time: number;\n}\n```\n\n#### UseTimeout\n\nCalls the function after the specified wait time (in milliseconds)\n\n```ts\ninterface Props {\n  fn: () =\u003e void;\n  time: number;\n}\n```\n\n### Sensors\n\n#### UseGeolocation\n\nTracks user's geographic location.\n\n```ts\ninterface Props {\n  fn: (\n    error: GeolocationPositionError | null,\n    data: GeolocationPosition | null\n  ) =\u003e void;\n  watch?: boolean;\n  options?: PositionOptions;\n}\n```\n\n### UI\n\n#### UseMouseOut\n\nFires a callback when mouse leaves target element.\n\n```ts\ninterface Props {\n  fn: () =\u003e void;\n  target: () =\u003e HTMLElement | Document | Window;\n  capture?: boolean;\n}\n```\n\n## License\n\n`react-unhook` is [MIT licensed](./LICENSE)\n\n[npm-badge]: https://img.shields.io/npm/v/react-unhook.svg?style=flat-square\n[npm-link]: https://www.npmjs.com/package/react-unhook\n[circle-badge]: https://img.shields.io/circleci/project/github/yeojz/react-unhook/master.svg?style=flat-square\n[circle-link]: https://circleci.com/gh/yeojz/react-unhook\n[type-ts-badge]: https://img.shields.io/badge/typedef-.d.ts-blue.svg?style=flat-square\u0026longCache=true\n[type-ts-link]: https://github.com/yeojz/react-unhook/tree/master/src/index.ts\n[codecov-badge]: https://img.shields.io/codecov/c/github/yeojz/react-unhook/master.svg?style=flat-square\n[codecov-link]: https://codecov.io/gh/yeojz/react-unhook\n[project-docs-link]: https://yeojz.github.io/react-unhook\n[with-state-local]: https://github.com/yeojz/react-unhook/blob/master/stories/withState.tsx\n[with-state-recompose]: https://github.com/acdlite/recompose/blob/master/docs/API.md#withstate\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyeojz%2Freact-unhook","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyeojz%2Freact-unhook","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyeojz%2Freact-unhook/lists"}