Hooks
Hooks solve a wide variety of connected problems most of which boil down to – "a system for reusable stateful logic".
Hooks afford one to use or re-use state and hook into a powerfull and composable lifecycle system.
There are state hooks like useState useReducer, useContext and useResource, lifecycle hooks like useEffect and useLayout and additional primitive hooks like useRef, useMemo and useCallback.
useState
Returns a tuple with a state value, and a function to update it, using the value
as the initial state.
const [state, setState] = useState(value)
When the initial value
is a function this is invoked with props as an argument.
const [state, setState] = useState(props => props.value)
The setState
function accepts a new state value and enqueues an update of the component.
setState(newValue)
However, new state functions are invoked with the value of the previous state value.
setState(prevValue => newValue)
Putting this together in a single counter example:
function Example (props) {
const [state, setState] = useState(0)
return h('main', {},
h('h1', {}, 'count:', state),
h('button', {onClick: event => setState(value => value + 1)}, '+'),
h('button', {onClick: event => setState(value => value - 1)}, '-')
)
}
useReducer
Returns a tuple with a state value, and a function to update it using a reducer
– and a value
as the initial state.
const [state, dispatch] = useReducer((state, action) => state, value)
Putting this together in a single counter example:
function Example (props) {
const [state, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'increment': return state + 1
case 'decrement': return state - 1
}
}, props => props.value)
return h('main', {},
h('h1', {}, 'count:', state),
h('button', {onClick: event => dispatch({type: 'increment'})}, '+'),
h('button', {onClick: event => dispatch({type: 'decrement'})}, '-')
)
}
useContext
Accepts a context provider that supplies a context value through Context.
useContext(
provider: function
)
Where "provider" is the component that provides the context value.
function Provider ({children}) {
return h(Context, {value: 0}, children)
}
and "context" the current context value.
function Consumer (props) {
const context = useContext(Provider)
}
Putting this together in an example:
function Provider ({children}) {
return h(Context, {value: 0}, children)
}
function Consumer (props) {
const context = useContext(Provider)
return h('main', {}, h('h1', {}, 'count:', context))
}
render(h(Provider, {}, h(Consumer)), document)
Unlike props context is accessible from any number of layers deep within a tree as long as there is an ancestor provider. That is to say a Consumer need not be the "immediate" child of a provider.
useResource
Accepts a resource callback that returns a promise.
useResource(
callback: function,
dependencies?: Array<any>
)
Where "callback" would provide the promise that resolves to the resource value, and "dependencies" is an optional array listing the values the resource depends on, insuring that when the values change the resource fetch is replayed. By default no dependencies indicates that the resource has no dependencies, resulting on only one fetch through the life of the resource bucket.
function Consumer (props) {
const resource = useResource(([]) => {
return fetch('reqres.in/api/users/2')
})
}
Putting this together in an example:
function Consumer ({page}) {
const resource = useResource(([page]) => {
return fetch('reqres.in/api/users/' + page)
}, [page])
return JSON.stringify(resource, null, 2)
}
render(h(Suspense, {}, h(Consumer)), document)
Both the useResource and useContext hooks can be employed in conjunction to afford access to a resource from any number of layers deep within a tree as long as there is an ancestor resource context provider.
Note that "useResource" optimally expects a suspense boundary ancestor somewhere within the tree.
useEffect
Accepts a function that contains imperative, possibly effectful procedures.
useEffect(
callback: function,
dependencies?: Array<any>
)
The use of effects spans multiple domains ranging from mutations, subscriptions, timers, logging, and other side effects that are not allowed inside the main body of a component.
function Example (props) {
useEffect(() => {
console.log('Hello')
}, [{}])
}
The function passed to useEffect will run after a render is committed to the screen. An effect can return a function that is executed once the effect goes through a cleanup phase, which can happen when the component unmounts or before a new effect is deployed.
function Example (props) {
useEffect(() => {
console.log('Hello')
return () => {
console.log('Bye!')
}
}, [])
}
By default, effects run after every completed render, but you can choose to deploy an effect when certain values have changed, or choose to only hook into a components mount and unmount phases by using an empty array.
function Example (props) {
useEffect(([value]) => {
console.log(value)
}, [props.value])
}
useLayout
The sibling hook of useEffect. Accepts a function that contains imperative, possibly layout thrashing procedures.
useLayout(
callback: function,
dependencies?: Array
)
Unlike useEffect "useLayout" fires synchronously after all user interface mutations. This can be used to read layout and synchronously re-render. Updates scheduled inside useLayout are flushed synchronously before layout paint.
function Example (props) {
useLayout(() => {
document.querySelector('*').forEach(current => {
current.style.marginLeft += '10px'
})
}, [])
}
useRef
Returns a mutable ref object whose current
property is initialized to the passed argument. Optionally accepts a function that is invoked with "props" as an argument before intializing to its return value. The returned object is persisted for the lifetime of the component.
const value = useRef(props => props.value)
An example use case in accessing child references imperatively:
function Example (props) {
const input = useRef(null)
return h('main', {},
h('input', {type: 'text', ref: input}),
h('button' {onClick: event => input.current.focus()}, 'Focus')
)
}
useMemo
Returns a memoized value.
const memoized = useMemo(([a, b]) => compute(a, b), [a, b])
Accepts a function and an array of dependencies. The function is invoked initally and when dependecies change.
This can serve as an optimization for expensive calculations.
An empty array may be used to signal a computation that should happen at most once in the lifecycle of a component.
const memoized = useMemo(() => compute(1, 2), [])
The same can be said if no dependencies array is provided, it is treated as an empty dependencies array.
useMemo(() => compute(1, 2)) === useMemo(() => compute(1, 2), [])
useCallback
Returns a memoized callback.
const callback = useCallback(([...args]) => compute(...args), [...args])
Like useMemo not providing the dependencies array acts like it would if an empty array was provided.