This project has evolved into mithril-hooks
Use hooks in Mithril.
- Deprecation
- Legacy
- Introduction
- Online demos
- Usage
- Compatibility
- Size
- Supported browsers
- History
- License
Use hook functions from the React Hooks API in Mithril:
useState
useEffect
useLayoutEffect
useReducer
useRef
useMemo
useCallback
- and custom hooks
import { withHooks } from "mithril-hookup"
const Counter = ({ useState, initialCount }) => {
const [count, setCount] = useState(initialCount)
return [
m("div", count),
m("button", {
onclick: () => setCount(count + 1)
}, "More")
]
}
const HookedCounter = withHooks(Counter)
m(HookedCounter, { initialCount: 1 })
Editable demos using the Flems playground:
- Simplest example
- 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-hookup
Use in code:
import { withHooks } from "mithril-hookup"
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, an optional second parameter can be passed to define if it should rerun:
useEffect(
() => {
document.title = `You clicked ${count} times`
},
[count] // Only re-run the effect if count changes
)
mithril-hookup 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 a hook function 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.
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:
setTicks(ticks => ticks + 1)
A setter function can be called from another hook:
const [inited, setInited] = useState(false)
useEffect(
() => {
setInited(true)
},
[/* empty array: only run at mount */]
)
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
)
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]
)
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:
const counterReducer = (state, action) => {
switch (action.type) {
case "increment":
return { count: state.count + 1 }
case "decrement":
return { count: state.count - 1 }
default:
throw new Error("Unhandled action:", action)
}
}
const Counter = ({ initialCount, useReducer }) => {
const initialState = { count: initialCount }
const [countState, dispatch] = useReducer(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 HookedCounter = withHooks(Counter)
m(HookedCounter, { initialCount: 0 })
The "ref" object is a generic container whose current
property is mutable and can hold any value.
const dom = useRef(null)
return [
m("div",
{
oncreate: vnode => dom.current = vnode.dom
},
count
)
]
To keep track of a value:
const Timer = ({ useState, useEffect, useRef }) => {
const [ticks, setTicks] = useState(0)
const intervalRef = useRef()
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"
)
]
}
const HookedTimer = withHooks(Timer)
Returns a memoized value.
const Counter = ({ count, useMemo }) => {
const memoizedValue = useMemo(
() => {
return computeExpensiveValue(count)
},
[count] // only recalculate when count is updated
)
// ...
}
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.
let previousCallback = null
const memoizedCallback = useCallback(
() => {
doSomething(a, b)
},
[a, b]
)
// Testing for reference equality:
if (previousCallback !== memoizedCallback) {
// New callback function created
previousCallback = memoizedCallback
memoizedCallback()
} else {
// Callback function is identical to the previous render
}
These React hooks make little sense with Mithril and are not included:
useContext
useImperativeHandle
useDebugValue
Custom hooks are created with a factory function. The function receives the default hooks (automatically), and should return an object with custom hook functions:
const customHooks = ({ useState /* or other default hooks required here */ }) => ({
useCount: (initialValue = 0) => {
const [count, setCount] = useState(initialValue)
return [
count, // value
() => setCount(count + 1), // increment
() => setCount(count - 1) // decrement
]
}
})
Pass the custom hooks function as second parameter to withHooks
:
const HookedCounter = withHooks(Counter, customHooks)
The custom hooks can now be used from the component:
const Counter = ({ useCount }) => {
const [count, increment, decrement] = useCount(0)
// ...
}
The complete code:
const customHooks = ({ useState }) => ({
useCount: (initialValue = 0) => {
const [count, setCount] = useState(initialValue)
return [
count, // value
() => setCount(count + 1), // increment
() => setCount(count - 1) // decrement
]
}
})
const Counter = ({ initialCount, useCount }) => {
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"
)
])
}
const HookedCounter = withHooks(Counter, customHooks)
m(HookedCounter, { initialCount: 0 })
withHooks
is a wrapper function around the function hookup
. It may be useful to know how this function works.
import { hookup } from "mithril-hookup"
const HookedCounter = hookup((vnode, { useState }) => {
const [count, setCount] = useState(vnode.attrs.initialCount)
return [
m("div", count),
m("button", {
onclick: () => setCount(count + 1)
}, "More")
]
})
m(HookedCounter, { initialCount: 1 })
The first parameter passed to hookup
is a wrapper function - also called a closure - that provides access to the original component vnode and the hook functions:
hookup(
(vnode, hookFunctions) => { /* returns a view */ }
)
Attributes passed to the component can be accessed through vnode
.
hookFunctions
is an object that contains the default hooks: useState
, useEffect
, useReducer
, etcetera, plus custom hooks:
const Counter = hookup((vnode, { useState }) => {
const initialCount = vnode.attrs.initialCount
const [count, setCount] = useState(initialCount)
return [
m("div", count),
m("button", {
onclick: () => setCount(count + 1)
}, "More")
]
})
m(Counter, { initialCount: 0 })
The custom hooks function is passed as second parameter to hookup
:
const Counter = hookup(
(
vnode,
{ useCount }
) => {
const [count, increment, decrement] = useCount(0)
// ...
},
customHooks
)
Child elements are accessed through the variable children
:
import { withHooks } from "mithril-hookup"
const Counter = ({ useState, initialCount, children }) => {
const [count, setCount] = useState(initialCount)
return [
m("div", count),
children
]
}
const HookedCounter = withHooks(Counter)
m(HookedCounter,
{ initialCount: 1 },
[
m("div", "This is a child element")
]
)
Tested with Mithril 1.1.6 and Mithril 2.x.
1.4 Kb gzipped
Output from npx browserslist
:
and_chr 71
and_ff 64
and_qq 1.2
and_uc 11.8
android 67
baidu 7.12
chrome 72
chrome 71
edge 18
edge 17
firefox 65
firefox 64
ie 11
ie_mob 11
ios_saf 12.0-12.1
ios_saf 11.3-11.4
op_mini all
op_mob 46
opera 57
safari 12
samsung 8.2
- Initial implementation: Barney Carroll (https://twitter.com/barneycarroll/status/1059865107679928320)
- Updated and enhanced by Arthur Clemens
MIT