All Projects → ArthurClemens → mithril-hooks

ArthurClemens / mithril-hooks

Licence: other
Use hooks in Mithril

Programming Languages

typescript
32286 projects
javascript
184084 projects - #8 most used programming language
CSS
56736 projects
HTML
75241 projects

Projects that are alternatives of or similar to mithril-hooks

create-mithril-app
Sets up a mithril.js project with webpack
Stars: ✭ 20 (-45.95%)
Mutual labels:  mithril
Mithril Infinite
Infinite scroll for Mithril
Stars: ✭ 83 (+124.32%)
Mutual labels:  mithril
Mithril Hx
Haxe externs for Mithril (JS MVC framework)
Stars: ✭ 178 (+381.08%)
Mutual labels:  mithril
Tutanota
Tutanota is an email service with a strong focus on security and privacy that lets you encrypt emails, contacts and calendar entries on all your devices.
Stars: ✭ 4,326 (+11591.89%)
Mutual labels:  mithril
Bell
⏱ Counting down to the next time the bell rings at school
Stars: ✭ 15 (-59.46%)
Mutual labels:  mithril
Mithril Isomorphic Example
Example of an isomorphic mithril application
Stars: ✭ 107 (+189.19%)
Mutual labels:  mithril
pingcrm-mithril
Ping CRM on Mithril.js - A mithril demo application to illustrate how Inertia.js works.
Stars: ✭ 22 (-40.54%)
Mutual labels:  mithril
Construct Ui
A Mithril.js UI library
Stars: ✭ 233 (+529.73%)
Mutual labels:  mithril
Lichobile
lichess.org mobile application
Stars: ✭ 1,043 (+2718.92%)
Mutual labels:  mithril
Awesome Mithril
A curated list of Mithril awesome
Stars: ✭ 155 (+318.92%)
Mutual labels:  mithril
Polythene
Material Design component library for Mithril and React
Stars: ✭ 578 (+1462.16%)
Mutual labels:  mithril
Todomvc Mithril
TodoMVC app using Mithril.js with CoffeeScript and Brunch
Stars: ✭ 15 (-59.46%)
Mutual labels:  mithril
Crucible
API CMS UI powered by Firebase, mithril, and my own dwindling sanity. Oh, and acronyms.
Stars: ✭ 116 (+213.51%)
Mutual labels:  mithril
Msx
JSX for Mithril.js 0.x
Stars: ✭ 370 (+900%)
Mutual labels:  mithril
Mithril.js
A JavaScript Framework for Building Brilliant Applications
Stars: ✭ 13,062 (+35202.7%)
Mutual labels:  mithril
mithril-select
Custom Select Component for Mithril.js
Stars: ✭ 14 (-62.16%)
Mutual labels:  mithril
Mithril Query
Query mithril virtual dom for testing purposes
Stars: ✭ 105 (+183.78%)
Mutual labels:  mithril
mithril-template-converter
Mithril HTML to JavaScript converter
Stars: ✭ 89 (+140.54%)
Mutual labels:  mithril
Mithril Node Render
Use mithril views to render server side
Stars: ✭ 195 (+427.03%)
Mutual labels:  mithril
Dnjs
DOM Notation JS
Stars: ✭ 118 (+218.92%)
Mutual labels:  mithril

mithril-hooks

Use hooks with Mithril.

Introduction

Use hook functions from the React Hooks API in Mithril:

  • useState
  • useEffect
  • useLayoutEffect
  • useReducer
  • useRef
  • useMemo
  • useCallback
  • and custom hooks

Online demos

Usage

npm install mithril-hooks
import { withHooks, useState /* and other hooks */ } from "mithril-hooks";

Example

// Toggle.ts

import m from 'mithril';
import { withHooks, useState } from 'mithril-hooks';

type ToggleProps = {
  isOn?: boolean;
};

const Toggle = withHooks(({ isOn }: ToggleProps) => {
  const [isOn, setIsOn] = useState<boolean>(isOn);

  return m('.toggle', [
    m('button',
      {
        onclick: () => setIsOn(current => !current),
      },
      'Toggle',
    ),
    m('div', isOn ? 'On' : 'Off'),
  ]);
});

Use the counter:

import { Toggle } from "./Toggle"

m(Toggle, { isOn: true })

Hooks and application logic

Hooks 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.

Custom hooks shows how to define and incorporate these hooks.

Rendering rules

With useState

Mithril's redraw is called when the state is initially set, and every time a state changes value.

With other hooks

Hook functions are always called at the first render.

For subsequent renders, a dependency list can be passed as second parameter to instruct when it should rerun:

useEffect(
  () => {
    document.title = `You clicked ${count} times`
  },
  [count] // Only re-run the effect if count changes
)

For the dependency list, mithril-hooks follows the React Hooks API:

  • Without a second argument: will run every render (Mithril lifecycle function view).
  • With an empty array: will only run at mount (Mithril lifecycle function oncreate).
  • With an array with variables: will only run whenever one of the variables has changed value (Mithril lifecycle function onupdate).

Note that effect hooks do not cause a re-render themselves.

Cleaning up

If useEffect returns a function, that function is called at unmount (Mithril lifecycle function onremove).

useEffect(
  () => {
    const subscription = subscribe()

    // Cleanup function:
    return () => {
      unsubscribe()
    }
  }
)

At cleanup Mithril's redraw is called.

API

withHooks

Higher order function that returns a component that works with hook functions.

type TAttrs = {};

const MyComponent = withHooks((attrs?: TAttrs) => {
  // Use hooks ...
  // Return a view:
  return m('div', 'My view')
});

The longhand version:

type TAttrs = {};

const RenderFn = (attrs?: TAttrs) => {
  // Use hooks ...
  // Return a view:
  return m('div', 'My view')
};

export const HookedComponent = withHooks<TAttrs>(RenderFn);

The returned HookedComponent can be called as any Mithril component:

m(HookedComponent, {
  // ... attrs
})

Options

Argument Type Required Description
renderFunction Function Yes Function with view logic
attrs Object No Attributes to pass to renderFunction

Signature

const withHooks: <T>(
  renderFunction: (attrs: T) => Vnode<T, {}> | Children,
  initialAttrs?: T
) => Component<T, {}>;

withHooks also receives vnode and children, where vnode includes the hook state. Extended signature:

const withHooks: <T>(
  renderFunction: (
    attrs: T & { vnode: Vnode<T, MithrilHooks.State>; children: Children },
  ) => Vnode<T, MithrilHooks.State> | Children,
  initialAttrs?: T,
) => Component<T, MithrilHooks.State>;

Default hooks

The React Hooks documentation provides excellent usage examples for default hooks. Let us suffice here with shorter descriptions.

useState

Provides the state value and a setter function:

const [count, setCount] = useState(0)

The setter function itself can pass a function - useful when values might otherwise be cached:

setCount(current => current + 1)

A setter function can be called from another hook:

const [inited, setInited] = useState(false)

useEffect(
  () => {
    setInited(true)
  },
  [/* empty array: only run at mount */]
)

Signature

const useState: <T>(initialValue?: T) => [
  T,
  (value: T | ((currentValue: T, index: number) => T)) => void
];

useEffect

Lets you perform side effects:

useEffect(
  () => {
    const className = "dark-mode"
    const element = window.document.body
    if (darkModeEnabled) {
      element.classList.add(className)
    } else {
      element.classList.remove(className)
    }
  },
  [darkModeEnabled] // Only re-run when value has changed
)

Signature

const useEffect: (
  fn: () => unknown | (() => unknown),
  deps?: unknown[],
) => void;

useLayoutEffect

Similar to useEffect, but fires synchronously after all DOM mutations. Use this when calculations must be done on DOM objects.

useLayoutEffect(
  () => {
    setMeasuredHeight(domElement.offsetHeight)
  },
  [screenSize]
)

Signature

const useLayoutEffect: (
  fn: () => unknown | (() => unknown),
  deps?: unknown[],
) => void;

useReducer

From the React docs:

An alternative to useState. Accepts a reducer of type (state, action) => newState, and returns the current state paired with a dispatch method. (If you’re familiar with Redux, you already know how this works.)

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.

Example:

import { withHooks, useReducer } from "mithril-hooks";

type TState = {
  count: number;
};

type TAction = {
  type: string;
};

const counterReducer = (state: TState, action: TAction) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error(`Unhandled action: ${action}`);
  }
};

type CounterAttrs = {
  initialCount: number;
};

const CounterFn = (attrs: CounterAttrs) => {
  const { initialCount } = attrs;
  const initialState = { count: initialCount }
  const [countState, dispatch] = useReducer<TState, TAction>(counterReducer, initialState)
  const count = countState.count

  return [
    m("div", count),
    m("button", {
      disabled: count === 0,
      onclick: () => dispatch({ type: "decrement" })
    }, "Less"),
    m("button", {
      onclick: () => dispatch({ type: "increment" })
    }, "More")
  ]
};

const Counter = withHooks(CounterFn);

m(Counter, { initialCount: 0 })

Signature

const useReducer: <T, A = void>(
  reducer: Reducer<T, A>,
  initialValue?: T | U,
  initFn?: (args: U) => T,
) => [T, (action: A) => void];

type Reducer<T, A> = (state: T, action: A) => T;

useRef

The "ref" object is a generic container whose current property is mutable and can hold any value.

const domRef = useRef<HTMLDivElement>(null)

return [
  m("div",
    {
      oncreate: vnode => dom.current = vnode.dom as HTMLDivElement
    },
    count
  )
]

To keep track of a value:

import { withHooks, useState, useEffect, useRef } from "mithril-hooks";

const Timer = withHooks(() => {
  const [ticks, setTicks] = useState(0)
  const intervalRef = useRef<number>()
  
  const handleCancelClick = () => {
    clearInterval(intervalRef.current)
    intervalRef.current = undefined
  }

  useEffect(
    () => {
      const intervalId = setInterval(() => {
        setTicks(ticks => ticks + 1)
      }, 1000)
      intervalRef.current = intervalId
      // Cleanup:
      return () => {
        clearInterval(intervalRef.current)
      }
    },
    [/* empty array: only run at mount */]
  )

  return [
    m("span", `Ticks: ${ticks}`),
    m("button", 
      {
        disabled: intervalRef.current === undefined,
        onclick: handleCancelClick
      },
      "Cancel"
    )
  ]
});

Signature

const useRef: <T>(initialValue?: T) => { current: T };

useMemo

Returns a memoized value.

import { withHooks, useMemo } from "mithril-hooks";

const computeExpensiveValue = (count: number): number => {
  // some computationally expensive function
  return count + Math.random();
};

const Counter = withHooks(({ count, useMemo }) => {
  const memoizedValue = useMemo(
    () => {
      return computeExpensiveValue(count)
    },
    [count] // only recalculate when count is updated
  )
  // Render ...
});

Signature

const useMemo: <T>(
  fn: MemoFn<T>,
  deps?: unknown[],
) => T;

type MemoFn<T> = () => T;

useCallback

Returns a memoized callback.

The function reference is unchanged in next renders (which makes a difference in performance expecially in React), but its return value will not be memoized.

const someCallback = (): number => {
  return Math.random();
};

type TCallback = () => void;
let previousCallback: TCallback;

const Callback = withHooks(() => {
  const [someValue, setSomeValue] = useState(0);

  const memoizedCallback = useCallback(() => {
    return someCallback();
  }, [someValue]);

  // Render ...
});

Signature

const const useCallback: <T>(
  fn: MemoFn<T>,
  deps?: unknown[],
) => MemoFn<T>;

type MemoFn<T> = () => T;

Omitted hooks

These React hooks make little sense with Mithril and are not included:

  • useContext
  • useImperativeHandle
  • useDebugValue

Custom hooks

// useCount.ts
import { useState } from "mithril-hooks";

export const useCount = (initialValue = 0) => {
  const [count, setCount] = useState(initialValue)
  return [
    count,                      // value
    () => setCount(count + 1),  // increment
    () => setCount(count - 1)   // decrement
  ]
}

Then use the custom hook:

// app.ts
import { withHooks } from "mithril-hooks";
import { useCount } from "./useCount";

type CounterAttrs = {
  initialCount: number;
};

const Counter = withHooks(({ initialCount }: CounterAttrs) => {
  const [count, increment, decrement] = useCount(initialCount)
  return m("div", [
    m("p", 
      `Count: ${count}`
    ),
    m("button", 
      {
        disabled: count === 0,
        onclick: () => decrement()
      },
      "Less"
    ),
    m("button", 
      {
        onclick: () => increment()
      },
      "More"
    )
  ])
});

m(Counter, { initialCount: 0 });

Children

Child elements can be accessed through the variable children. See mithril-hooks - Child elements.

type CounterAttrs = {
  initialCount: number;
  children?: Children;
};

const Counter = withHooks(({ initialCount, children }: CounterAttrs) => {
  const [count, setCount] = useState(initialCount);
  return [
    m("div", `Count: ${count}`),
    m(
      "button",
      {
        disabled: count === 0,
        onclick: () => setCount((c) => c - 1)
      },
      "Less"
    ),
    m(
      "button",
      {
        onclick: () => setCount((c) => c + 1)
      },
      "More"
    ),
    children
  ];
});

const App = {
  view: () =>
    m(Counter, { initialCount: 1 }, [m("div", "This is a child element")])
};

Troubleshooting

TypeError: Cannot read property 'depsIndex' of undefined

Possibly several instances of mithril-hooks are referenced. Prevent this by pointing the transpiler to a single instance.

When using Webpack, add to the config:

resolve: {
  // Make sure that libs are included only once
  alias: {
    'mithril-hooks': path.resolve(baseDir, 'node_modules/mithril-hooks'),
  },
},

Compatibility

Tested with Mithril 1.1.6 and Mithril 2.x.

Sizes

┌───────────────────────────────────────────┐
│                                           │
│   Bundle Name:  mithril-hooks.module.js   │
│   Bundle Size:  5.96 KB                   │
│   Minified Size:  2.75 KB                 │
│   Gzipped Size:  1.19 KB                  │
│                                           │
└───────────────────────────────────────────┘

┌────────────────────────────────────────┐
│                                        │
│   Bundle Name:  mithril-hooks.umd.js   │
│   Bundle Size:  6.95 KB                │
│   Minified Size:  2.57 KB              │
│   Gzipped Size:  1.24 KB               │
│                                        │
└────────────────────────────────────────┘

┌─────────────────────────────────────┐
│                                     │
│   Bundle Name:  mithril-hooks.cjs   │
│   Bundle Size:  6.18 KB             │
│   Minified Size:  2.96 KB           │
│   Gzipped Size:  1.26 KB            │
│                                     │
└─────────────────────────────────────┘

History

License

MIT

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].