{"id":25234596,"url":"https://github.com/arthurclemens/mithril-hooks","last_synced_at":"2025-04-09T19:18:05.828Z","repository":{"id":40813310,"uuid":"178091351","full_name":"ArthurClemens/mithril-hooks","owner":"ArthurClemens","description":"Use hooks in Mithril","archived":false,"fork":false,"pushed_at":"2025-04-05T03:14:59.000Z","size":2928,"stargazers_count":41,"open_issues_count":12,"forks_count":4,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-04-09T19:17:58.368Z","etag":null,"topics":["hooks","hooks-api-react","mithril"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","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/ArthurClemens.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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-27T23:44:27.000Z","updated_at":"2025-01-17T16:59:54.000Z","dependencies_parsed_at":"2024-06-04T11:47:14.629Z","dependency_job_id":"ddc15d2f-6fd8-4e92-8178-f2145f5ef557","html_url":"https://github.com/ArthurClemens/mithril-hooks","commit_stats":{"total_commits":96,"total_committers":3,"mean_commits":32.0,"dds":"0.36458333333333337","last_synced_commit":"9bfaaad3dc30c48d0540259f80e071c7b1849a40"},"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArthurClemens%2Fmithril-hooks","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArthurClemens%2Fmithril-hooks/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArthurClemens%2Fmithril-hooks/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArthurClemens%2Fmithril-hooks/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ArthurClemens","download_url":"https://codeload.github.com/ArthurClemens/mithril-hooks/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248094989,"owners_count":21046770,"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":["hooks","hooks-api-react","mithril"],"created_at":"2025-02-11T14:00:01.758Z","updated_at":"2025-04-09T19:18:05.797Z","avatar_url":"https://github.com/ArthurClemens.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# mithril-hooks\n\nUse hooks with Mithril.\n\n- [Introduction](#introduction)\n- [Online demos](#online-demos)\n- [Usage](#usage)\n  - [Example](#example)\n  - [Hooks and application logic](#hooks-and-application-logic)\n  - [Rendering rules](#rendering-rules)\n    - [With useState](#with-usestate)\n    - [With other hooks](#with-other-hooks)\n    - [Cleaning up](#cleaning-up)\n- [API](#api)\n  - [withHooks](#withhooks)\n  - [Default hooks](#default-hooks)\n    - [useState](#usestate)\n    - [useEffect](#useeffect)\n    - [useLayoutEffect](#uselayouteffect)\n    - [useReducer](#usereducer)\n    - [useRef](#useref)\n    - [useMemo](#usememo)\n    - [useCallback](#usecallback)\n    - [Omitted hooks](#omitted-hooks)\n  - [Custom hooks](#custom-hooks)\n  - [Children](#children)\n- [Troubleshooting](#troubleshooting)\n  - [TypeError: Cannot read property 'depsIndex' of undefined](#typeerror-cannot-read-property-depsindex-of-undefined)\n- [Compatibility](#compatibility)\n- [Sizes](#sizes)\n- [History](#history)\n- [License](#license)\n\n\n## Introduction\n\nUse hook functions from the [React Hooks API](https://reactjs.org/docs/hooks-intro.html) in Mithril:\n\n* `useState`\n* `useEffect`\n* `useLayoutEffect`\n* `useReducer`\n* `useRef`\n* `useMemo`\n* `useCallback`\n* and custom hooks\n\n\n## Online demos\n\n* [Simple counter with useState](https://codesandbox.io/s/mithril-hooks-usestate-box04)\n* [Simple form handling with useState](https://codesandbox.io/s/mithril-hooks-simple-form-handling-with-usestate-qxpzr)\n* [\"Building Your Own Hooks\" chat API example](https://codesandbox.io/s/mithril-hooks-building-your-own-hooks-chat-api-example-tmmhc) - this example roughly follows the [React documentation on custom hooks](https://reactjs.org/docs/hooks-custom.html)\n* [Custom hooks and useReducer](https://codesandbox.io/s/mithril-hooks-custom-hooks-and-usereducer-hurn8)\n* [Custom hooks to search iTunes with a debounce function](https://codesandbox.io/s/mithril-hooks-using-useeffects-with-a-debounce-function-m3p8u)\n  \n## Usage\n\n```bash\nnpm install mithril-hooks\n```\n\n```ts\nimport { withHooks, useState /* and other hooks */ } from \"mithril-hooks\";\n```\n\n### Example\n\n```ts\n// Toggle.ts\n\nimport m from 'mithril';\nimport { withHooks, useState } from 'mithril-hooks';\n\ntype ToggleProps = {\n  isOn?: boolean;\n};\n\nconst Toggle = withHooks(({ isOn }: ToggleProps) =\u003e {\n  const [isOn, setIsOn] = useState\u003cboolean\u003e(isOn);\n\n  return m('.toggle', [\n    m('button',\n      {\n        onclick: () =\u003e setIsOn(current =\u003e !current),\n      },\n      'Toggle',\n    ),\n    m('div', isOn ? 'On' : 'Off'),\n  ]);\n});\n\n```\n\nUse the counter:\n```ts\nimport { Toggle } from \"./Toggle\"\n\nm(Toggle, { isOn: true })\n```\n\n\n\n### Hooks and application logic\n\nHooks can be defined outside of the component, imported from other files. This makes it possible to define utility functions to be shared across the application.\n\n[Custom hooks](#custom-hooks) shows how to define and incorporate these hooks.\n\n\n\n### Rendering rules\n\n#### With useState\n\nMithril's `redraw` is called when the state is initially set, and every time a state changes value.\n\n\n#### With other hooks\n\nHook functions are always called at the first render.\n\nFor subsequent renders, a dependency list can be passed as second parameter to instruct when it should rerun:\n\n```javascript\nuseEffect(\n  () =\u003e {\n    document.title = `You clicked ${count} times`\n  },\n  [count] // Only re-run the effect if count changes\n)\n```\n\nFor the dependency list, `mithril-hooks` follows the React Hooks API:\n\n* Without a second argument: will run every render (Mithril lifecycle function [view](https://mithril.js.org/index.html#components)).\n* With an empty array: will only run at mount (Mithril lifecycle function [oncreate](https://mithril.js.org/lifecycle-methods.html#oncreate)).\n* With an array with variables: will only run whenever one of the variables has changed value (Mithril lifecycle function [onupdate](https://mithril.js.org/lifecycle-methods.html#onupdate)).\n\n\nNote that effect hooks do not cause a re-render themselves.\n\n\n#### Cleaning up\n\nIf `useEffect` returns a function, that function is called at unmount (Mithril lifecycle function [onremove](https://mithril.js.org/lifecycle-methods.html#onremove)).\n\n```javascript\nuseEffect(\n  () =\u003e {\n    const subscription = subscribe()\n\n    // Cleanup function:\n    return () =\u003e {\n      unsubscribe()\n    }\n  }\n)\n```\n\nAt cleanup Mithril's `redraw` is called.\n\n\n## API\n\n### withHooks\n\nHigher order function that returns a component that works with hook functions.\n\n\n```ts\ntype TAttrs = {};\n\nconst MyComponent = withHooks((attrs?: TAttrs) =\u003e {\n  // Use hooks ...\n  // Return a view:\n  return m('div', 'My view')\n});\n```\n\nThe longhand version:\n\n```ts\ntype TAttrs = {};\n\nconst RenderFn = (attrs?: TAttrs) =\u003e {\n  // Use hooks ...\n  // Return a view:\n  return m('div', 'My view')\n};\n\nexport const HookedComponent = withHooks\u003cTAttrs\u003e(RenderFn);\n```\n\nThe returned `HookedComponent` can be called as any Mithril component:\n\n```ts\nm(HookedComponent, {\n  // ... attrs\n})\n```\n\n\n\n**Options**\n\n| **Argument**     | **Type** | **Required** | **Description**                        |\n| ---------------- | -------- | ------------ | -------------------------------------- |\n| `renderFunction` | Function | Yes          | Function with view logic               |\n| `attrs`          | Object   | No           | Attributes to pass to `renderFunction` |\n\n\n**Signature**\n\n```ts\nconst withHooks: \u003cT\u003e(\n  renderFunction: (attrs: T) =\u003e Vnode\u003cT, {}\u003e | Children,\n  initialAttrs?: T\n) =\u003e Component\u003cT, {}\u003e;\n```\n\n`withHooks` also receives `vnode` and `children`, where `vnode` includes the hook state. Extended signature:\n\n```ts\nconst withHooks: \u003cT\u003e(\n  renderFunction: (\n    attrs: T \u0026 { vnode: Vnode\u003cT, MithrilHooks.State\u003e; children: Children },\n  ) =\u003e Vnode\u003cT, MithrilHooks.State\u003e | Children,\n  initialAttrs?: T,\n) =\u003e Component\u003cT, MithrilHooks.State\u003e;\n```\n\n\n\n\n### Default hooks\n\nThe [React Hooks documentation](https://reactjs.org/docs/hooks-intro.html) provides excellent usage examples for default hooks. Let us suffice here with shorter descriptions.\n\n\n#### useState\n\nProvides the state value and a setter function:\n\n```ts\nconst [count, setCount] = useState(0)\n```\n\nThe setter function itself can pass a function - useful when values might otherwise be cached:\n\n```javascript\nsetCount(current =\u003e current + 1)\n```\n\nA setter function can be called from another hook:\n\n```javascript\nconst [inited, setInited] = useState(false)\n\nuseEffect(\n  () =\u003e {\n    setInited(true)\n  },\n  [/* empty array: only run at mount */]\n)\n```\n\n**Signature**\n\n```ts\nconst useState: \u003cT\u003e(initialValue?: T) =\u003e [\n  T,\n  (value: T | ((currentValue: T, index: number) =\u003e T)) =\u003e void\n];\n```\n\n\n#### useEffect\n\nLets you perform side effects:\n\n```javascript\nuseEffect(\n  () =\u003e {\n    const className = \"dark-mode\"\n    const element = window.document.body\n    if (darkModeEnabled) {\n      element.classList.add(className)\n    } else {\n      element.classList.remove(className)\n    }\n  },\n  [darkModeEnabled] // Only re-run when value has changed\n)\n```\n\n**Signature**\n\n```ts\nconst useEffect: (\n  fn: () =\u003e unknown | (() =\u003e unknown),\n  deps?: unknown[],\n) =\u003e void;\n```\n\n#### useLayoutEffect\n\nSimilar to `useEffect`, but fires synchronously after all DOM mutations. Use this when calculations must be done on DOM objects.\n\n```javascript\nuseLayoutEffect(\n  () =\u003e {\n    setMeasuredHeight(domElement.offsetHeight)\n  },\n  [screenSize]\n)\n```\n\n**Signature**\n\n```ts\nconst useLayoutEffect: (\n  fn: () =\u003e unknown | (() =\u003e unknown),\n  deps?: unknown[],\n) =\u003e void;\n```\n\n#### useReducer\n\nFrom the [React docs](https://reactjs.org/docs/hooks-reference.html#usereducer):\n\n\u003e An alternative to useState. Accepts a reducer of type `(state, action) =\u003e newState`, and returns the current state paired with a `dispatch` method. (If you’re familiar with Redux, you already know how this works.)\n\u003e \n\u003e `useReducer` is usually preferable to `useState` when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.\n\nExample:\n\n```ts\nimport { withHooks, useReducer } from \"mithril-hooks\";\n\ntype TState = {\n  count: number;\n};\n\ntype TAction = {\n  type: string;\n};\n\nconst counterReducer = (state: TState, action: TAction) =\u003e {\n  switch (action.type) {\n    case 'increment':\n      return { count: state.count + 1 };\n    case 'decrement':\n      return { count: state.count - 1 };\n    default:\n      throw new Error(`Unhandled action: ${action}`);\n  }\n};\n\ntype CounterAttrs = {\n  initialCount: number;\n};\n\nconst CounterFn = (attrs: CounterAttrs) =\u003e {\n  const { initialCount } = attrs;\n  const initialState = { count: initialCount }\n  const [countState, dispatch] = useReducer\u003cTState, TAction\u003e(counterReducer, initialState)\n  const count = countState.count\n\n  return [\n    m(\"div\", count),\n    m(\"button\", {\n      disabled: count === 0,\n      onclick: () =\u003e dispatch({ type: \"decrement\" })\n    }, \"Less\"),\n    m(\"button\", {\n      onclick: () =\u003e dispatch({ type: \"increment\" })\n    }, \"More\")\n  ]\n};\n\nconst Counter = withHooks(CounterFn);\n\nm(Counter, { initialCount: 0 })\n```\n\n**Signature**\n\n```ts\nconst useReducer: \u003cT, A = void\u003e(\n  reducer: Reducer\u003cT, A\u003e,\n  initialValue?: T | U,\n  initFn?: (args: U) =\u003e T,\n) =\u003e [T, (action: A) =\u003e void];\n\ntype Reducer\u003cT, A\u003e = (state: T, action: A) =\u003e T;\n```\n\n#### useRef\n\nThe \"ref\" object is a generic container whose `current` property is mutable and can hold any value.\n\n```ts\nconst domRef = useRef\u003cHTMLDivElement\u003e(null)\n\nreturn [\n  m(\"div\",\n    {\n      oncreate: vnode =\u003e dom.current = vnode.dom as HTMLDivElement\n    },\n    count\n  )\n]\n```\n\nTo keep track of a value:\n\n```ts\nimport { withHooks, useState, useEffect, useRef } from \"mithril-hooks\";\n\nconst Timer = withHooks(() =\u003e {\n  const [ticks, setTicks] = useState(0)\n  const intervalRef = useRef\u003cnumber\u003e()\n  \n  const handleCancelClick = () =\u003e {\n    clearInterval(intervalRef.current)\n    intervalRef.current = undefined\n  }\n\n  useEffect(\n    () =\u003e {\n      const intervalId = setInterval(() =\u003e {\n        setTicks(ticks =\u003e ticks + 1)\n      }, 1000)\n      intervalRef.current = intervalId\n      // Cleanup:\n      return () =\u003e {\n        clearInterval(intervalRef.current)\n      }\n    },\n    [/* empty array: only run at mount */]\n  )\n\n  return [\n    m(\"span\", `Ticks: ${ticks}`),\n    m(\"button\", \n      {\n        disabled: intervalRef.current === undefined,\n        onclick: handleCancelClick\n      },\n      \"Cancel\"\n    )\n  ]\n});\n```\n\n**Signature**\n\n```ts\nconst useRef: \u003cT\u003e(initialValue?: T) =\u003e { current: T };\n```\n\n#### useMemo\n\nReturns a memoized value.\n\n```ts\nimport { withHooks, useMemo } from \"mithril-hooks\";\n\nconst computeExpensiveValue = (count: number): number =\u003e {\n  // some computationally expensive function\n  return count + Math.random();\n};\n\nconst Counter = withHooks(({ count, useMemo }) =\u003e {\n  const memoizedValue = useMemo(\n    () =\u003e {\n      return computeExpensiveValue(count)\n    },\n    [count] // only recalculate when count is updated\n  )\n  // Render ...\n});\n```\n\n**Signature**\n\n```ts\nconst useMemo: \u003cT\u003e(\n  fn: MemoFn\u003cT\u003e,\n  deps?: unknown[],\n) =\u003e T;\n\ntype MemoFn\u003cT\u003e = () =\u003e T;\n```\n\n#### useCallback\n\nReturns a memoized callback.\n\nThe function reference is unchanged in next renders (which makes a difference in performance expecially in React), but its return value will not be memoized.\n\n```ts\nconst someCallback = (): number =\u003e {\n  return Math.random();\n};\n\ntype TCallback = () =\u003e void;\nlet previousCallback: TCallback;\n\nconst Callback = withHooks(() =\u003e {\n  const [someValue, setSomeValue] = useState(0);\n\n  const memoizedCallback = useCallback(() =\u003e {\n    return someCallback();\n  }, [someValue]);\n\n  // Render ...\n});\n```\n\n**Signature**\n\n```ts\nconst const useCallback: \u003cT\u003e(\n  fn: MemoFn\u003cT\u003e,\n  deps?: unknown[],\n) =\u003e MemoFn\u003cT\u003e;\n\ntype MemoFn\u003cT\u003e = () =\u003e T;\n```\n\n\n#### Omitted hooks\n\nThese React hooks make little sense with Mithril and are not included:\n\n* `useContext`\n* `useImperativeHandle`\n* `useDebugValue`\n\n### Custom hooks\n\n```ts\n// useCount.ts\nimport { useState } from \"mithril-hooks\";\n\nexport const useCount = (initialValue = 0) =\u003e {\n  const [count, setCount] = useState(initialValue)\n  return [\n    count,                      // value\n    () =\u003e setCount(count + 1),  // increment\n    () =\u003e setCount(count - 1)   // decrement\n  ]\n}\n```\n\nThen use the custom hook:\n\n```ts\n// app.ts\nimport { withHooks } from \"mithril-hooks\";\nimport { useCount } from \"./useCount\";\n\ntype CounterAttrs = {\n  initialCount: number;\n};\n\nconst Counter = withHooks(({ initialCount }: CounterAttrs) =\u003e {\n  const [count, increment, decrement] = useCount(initialCount)\n  return m(\"div\", [\n    m(\"p\", \n      `Count: ${count}`\n    ),\n    m(\"button\", \n      {\n        disabled: count === 0,\n        onclick: () =\u003e decrement()\n      },\n      \"Less\"\n    ),\n    m(\"button\", \n      {\n        onclick: () =\u003e increment()\n      },\n      \"More\"\n    )\n  ])\n});\n\nm(Counter, { initialCount: 0 });\n```\n\n### Children\n\nChild elements can be accessed through the variable `children`. See [mithril-hooks - Child elements](https://codesandbox.io/s/mithril-hooks-child-elements-6i8r1).\n\n```ts\ntype CounterAttrs = {\n  initialCount: number;\n  children?: Children;\n};\n\nconst Counter = withHooks(({ initialCount, children }: CounterAttrs) =\u003e {\n  const [count, setCount] = useState(initialCount);\n  return [\n    m(\"div\", `Count: ${count}`),\n    m(\n      \"button\",\n      {\n        disabled: count === 0,\n        onclick: () =\u003e setCount((c) =\u003e c - 1)\n      },\n      \"Less\"\n    ),\n    m(\n      \"button\",\n      {\n        onclick: () =\u003e setCount((c) =\u003e c + 1)\n      },\n      \"More\"\n    ),\n    children\n  ];\n});\n\nconst App = {\n  view: () =\u003e\n    m(Counter, { initialCount: 1 }, [m(\"div\", \"This is a child element\")])\n};\n```\n\n\n## Troubleshooting\n\n### TypeError: Cannot read property 'depsIndex' of undefined\n\nPossibly several instances of `mithril-hooks` are referenced. Prevent this by pointing the transpiler to a single instance.\n\nWhen using Webpack, add to the config:\n\n```js\nresolve: {\n  // Make sure that libs are included only once\n  alias: {\n    'mithril-hooks': path.resolve(baseDir, 'node_modules/mithril-hooks'),\n  },\n},\n```\n\n\n## Compatibility\n\nTested with Mithril 1.1.6 and Mithril 2.x.\n\n\n## Sizes\n\n```\n┌───────────────────────────────────────────┐\n│                                           │\n│   Bundle Name:  mithril-hooks.module.js   │\n│   Bundle Size:  5.96 KB                   │\n│   Minified Size:  2.75 KB                 │\n│   Gzipped Size:  1.19 KB                  │\n│                                           │\n└───────────────────────────────────────────┘\n\n┌────────────────────────────────────────┐\n│                                        │\n│   Bundle Name:  mithril-hooks.umd.js   │\n│   Bundle Size:  6.95 KB                │\n│   Minified Size:  2.57 KB              │\n│   Gzipped Size:  1.24 KB               │\n│                                        │\n└────────────────────────────────────────┘\n\n┌─────────────────────────────────────┐\n│                                     │\n│   Bundle Name:  mithril-hooks.cjs   │\n│   Bundle Size:  6.18 KB             │\n│   Minified Size:  2.96 KB           │\n│   Gzipped Size:  1.26 KB            │\n│                                     │\n└─────────────────────────────────────┘\n```\n\n\n## History\n\n* Initial version: [Barney Carroll](https://twitter.com/barneycarroll/status/1059865107679928320)\n* Updated and enhanced by Arthur Clemens with support from [Isiah Meadows](https://github.com/isiahmeadows)\n\n\n## License\n\nMIT\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farthurclemens%2Fmithril-hooks","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Farthurclemens%2Fmithril-hooks","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farthurclemens%2Fmithril-hooks/lists"}