Use hooks with Mithril.
Use hook functions from the React Hooks API in Mithril:
useState
useEffect
useLayoutEffect
useReducer
useRef
useMemo
useCallback
- and custom hooks
- Simple counter with useState
- Simple form handling with useState
- "Building Your Own Hooks" chat API example - this example roughly follows the React documentation on custom hooks
- Custom hooks and useReducer
- Custom hooks to search iTunes with a debounce function
npm install mithril-hooks
import { withHooks, useState /* and other hooks */ } from "mithril-hooks";
// 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 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.
Mithril's redraw
is called when the state is initially set, and every time a state changes value.
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.
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.
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>;
The React Hooks documentation provides excellent usage examples for default hooks. Let us suffice here with shorter descriptions.
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
];
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;
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;
From the React docs:
An alternative to useState. Accepts a reducer of type
(state, action) => newState
, and returns the current state paired with adispatch
method. (If you’re familiar with Redux, you already know how this works.)
useReducer
is usually preferable touseState
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;
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 };
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;
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;
These React hooks make little sense with Mithril and are not included:
useContext
useImperativeHandle
useDebugValue
// 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 });
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")])
};
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'),
},
},
Tested with Mithril 1.1.6 and Mithril 2.x.
┌───────────────────────────────────────────┐
│ │
│ 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 │
│ │
└─────────────────────────────────────┘
- Initial version: Barney Carroll
- Updated and enhanced by Arthur Clemens with support from Isiah Meadows
MIT