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.