diff --git a/src/useCallbackRef.ts b/src/useCallbackRef.ts
index 9bcff27..6587b59 100644
--- a/src/useCallbackRef.ts
+++ b/src/useCallbackRef.ts
@@ -21,6 +21,8 @@ import { useState } from 'react'
*
* return
* ```
+ *
+ * @category refs
*/
export default function useCallbackRef(): [
TValue | null,
diff --git a/src/useImmediateUpdateEffect.ts b/src/useImmediateUpdateEffect.ts
index f67f488..b5590e2 100644
--- a/src/useImmediateUpdateEffect.ts
+++ b/src/useImmediateUpdateEffect.ts
@@ -14,6 +14,8 @@ import useStableMemo from './useStableMemo'
* setValue(value)
* }, [value])
* ```
+ *
+ * @category effects
*/
function useImmediateUpdateEffect(effect: () => void, deps: DependencyList) {
const firstRef = useRef(true)
diff --git a/src/useIsomorphicEffect.ts b/src/useIsomorphicEffect.ts
index 73086d9..2848107 100644
--- a/src/useIsomorphicEffect.ts
+++ b/src/useIsomorphicEffect.ts
@@ -14,5 +14,7 @@ const isDOM = typeof document !== 'undefined'
* Only useful to avoid the console warning.
*
* PREFER `useEffect` UNLESS YOU KNOW WHAT YOU ARE DOING.
+ *
+ * @category effects
*/
export default isDOM || isReactNative ? useLayoutEffect : useEffect
diff --git a/src/useMergedRefs.ts b/src/useMergedRefs.ts
index be42c28..cadfc3b 100644
--- a/src/useMergedRefs.ts
+++ b/src/useMergedRefs.ts
@@ -33,6 +33,7 @@ export function mergeRefs(refA?: Ref | null, refB?: Ref | null) {
*
* @param refA A Callback or mutable Ref
* @param refB A Callback or mutable Ref
+ * @category refs
*/
function useMergedRefs(refA?: Ref | null, refB?: Ref | null) {
return useMemo(() => mergeRefs(refA, refB), [refA, refB])
diff --git a/src/useMountEffect.ts b/src/useMountEffect.ts
new file mode 100644
index 0000000..a4ed27f
--- /dev/null
+++ b/src/useMountEffect.ts
@@ -0,0 +1,24 @@
+import { useEffect, EffectCallback } from 'react'
+
+/**
+ * Run's an effect on mount, and is cleaned up on unmount. Generally
+ * useful for interop with non-react plugins or components
+ *
+ * ```ts
+ * useMountEffect(() => {
+ * const plugin = $.myPlugin(ref.current)
+ *
+ * return () => {
+ * plugin.destroy()
+ * }
+ * })
+ * ```
+ * @param effect An effect to run on mount
+ *
+ * @category effects
+ */
+function useMountEffect(effect: EffectCallback) {
+ return useEffect(effect, [])
+}
+
+export default useMountEffect
diff --git a/src/useRefWithInitialValueFactory.ts b/src/useRefWithInitialValueFactory.ts
new file mode 100644
index 0000000..6ca1f88
--- /dev/null
+++ b/src/useRefWithInitialValueFactory.ts
@@ -0,0 +1,25 @@
+import { useRef } from 'react'
+
+const dft: any = Symbol('default value sigil')
+
+/**
+ * Exactly the same as `useRef` except that the initial value is set via a
+ * factroy function. Useful when the default is relatively costly to construct.
+ *
+ * ```ts
+ * const ref = useRefWithInitialValueFactory(() => constructExpensiveValue())
+ *
+ * ```
+ *
+ * @param initialValueFactory A factory function returning the ref's default value
+ * @category refs
+ */
+export default function useRefWithInitialValueFactory(
+ initialValueFactory: () => T,
+) {
+ const ref = useRef(dft)
+ if (ref.current === dft) {
+ ref.current = initialValueFactory()
+ }
+ return ref
+}
diff --git a/src/useUpdateEffect.ts b/src/useUpdateEffect.ts
new file mode 100644
index 0000000..7e56ffe
--- /dev/null
+++ b/src/useUpdateEffect.ts
@@ -0,0 +1,34 @@
+import { useEffect, EffectCallback, DependencyList, useRef } from 'react'
+
+/**
+ * Runs an effect only when the dependencies have changed, skipping the
+ * initial "on mount" run. Caution, if the dependency list never changes,
+ * the effect is **never run**
+ *
+ * ```ts
+ * const ref = useRef(null);
+ *
+ * // focuses an element only if the focus changes, and not on mount
+ * useUpdateEffect(() => {
+ * const element = ref.current?.children[focusedIdx] as HTMLElement
+ *
+ * element?.focus()
+ *
+ * }, [focusedIndex])
+ * ```
+ * @param effect An effect to run on mount
+ *
+ * @category effects
+ */
+function useUpdateEffect(fn: EffectCallback, deps: DependencyList) {
+ const isFirst = useRef(true)
+ useEffect(() => {
+ if (isFirst.current) {
+ isFirst.current = false
+ return
+ }
+ return fn()
+ }, deps)
+}
+
+export default useUpdateEffect
diff --git a/src/useUpdatedRef.ts b/src/useUpdatedRef.ts
index 449608f..b2b5b78 100644
--- a/src/useUpdatedRef.ts
+++ b/src/useUpdatedRef.ts
@@ -4,6 +4,7 @@ import { useRef } from 'react'
* Returns a ref that is immediately updated with the new value
*
* @param value The Ref value
+ * @category refs
*/
export default function useUpdatedRef(value: T) {
const valueRef = useRef(value)
diff --git a/src/useWillUnmount.ts b/src/useWillUnmount.ts
index 51f10dc..b3bbf56 100644
--- a/src/useWillUnmount.ts
+++ b/src/useWillUnmount.ts
@@ -5,6 +5,7 @@ import { useEffect } from 'react'
* Attach a callback that fires when a component unmounts
*
* @param fn Handler to run when the component unmounts
+ * @category effects
*/
export default function useWillUnmount(fn: () => void) {
const onUnmount = useUpdatedRef(fn)
diff --git a/test/useImmediateUpdateEffect.test.tsx b/test/useImmediateUpdateEffect.test.tsx
index bfb7696..b7a645d 100644
--- a/test/useImmediateUpdateEffect.test.tsx
+++ b/test/useImmediateUpdateEffect.test.tsx
@@ -2,7 +2,7 @@ import useImmediateUpdateEffect from '../src/useImmediateUpdateEffect'
import { renderHook } from './helpers'
describe('useImmediateUpdateEffect', () => {
- it('should return a function that returns mount state', () => {
+ it('should run update after value changes', () => {
const spy = jest.fn()
const [, wrapper] = renderHook(
diff --git a/test/useMountEffect.test.tsx b/test/useMountEffect.test.tsx
new file mode 100644
index 0000000..d09e6b6
--- /dev/null
+++ b/test/useMountEffect.test.tsx
@@ -0,0 +1,30 @@
+import useMountEffect from '../src/useMountEffect'
+import { renderHook } from './helpers'
+
+describe('useMountEffect', () => {
+ it('should run update only on mount', () => {
+ const teardown = jest.fn()
+ const spy = jest.fn(() => teardown)
+
+ const [, wrapper] = renderHook(
+ () => {
+ useMountEffect(spy)
+ },
+ { value: 1, other: false },
+ )
+
+ expect(spy).toHaveBeenCalledTimes(1)
+
+ wrapper.setProps({ value: 2 })
+
+ expect(spy).toHaveBeenCalledTimes(1)
+
+ wrapper.setProps({ value: 2, other: true })
+
+ expect(spy).toHaveBeenCalledTimes(1)
+
+ wrapper.unmount()
+
+ expect(teardown).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/test/useRefWithInitialValueFactory.test.tsx b/test/useRefWithInitialValueFactory.test.tsx
new file mode 100644
index 0000000..8e817fc
--- /dev/null
+++ b/test/useRefWithInitialValueFactory.test.tsx
@@ -0,0 +1,25 @@
+import useRefWithInitialValueFactory from '../src/useRefWithInitialValueFactory'
+import { renderHook } from './helpers'
+
+describe('useRefWithInitialValueFactory', () => {
+ it('should set a ref value using factory once', () => {
+ const spy = jest.fn((v: number) => v)
+
+ const [ref, wrapper] = renderHook(
+ ({ value }) => {
+ return useRefWithInitialValueFactory(() => spy(value))
+ },
+ { value: 2 },
+ )
+
+ expect(spy).toHaveBeenCalledTimes(1)
+ expect(spy).toHaveBeenCalledWith(2)
+
+ expect(ref.current).toEqual(2)
+
+ wrapper.setProps({ value: 1 })
+
+ expect(spy).toHaveBeenCalledTimes(1)
+ expect(ref.current).toEqual(2)
+ })
+})
diff --git a/test/useUpdateEffect.test.tsx b/test/useUpdateEffect.test.tsx
new file mode 100644
index 0000000..703b278
--- /dev/null
+++ b/test/useUpdateEffect.test.tsx
@@ -0,0 +1,34 @@
+import useUpdateEffect from '../src/useUpdateEffect'
+import { renderHook } from './helpers'
+
+describe('useUpdateEffect', () => {
+ it('should run update after value changes', () => {
+ const teardown = jest.fn()
+ const spy = jest.fn(() => teardown)
+
+ const [, wrapper] = renderHook(
+ ({ value }) => {
+ useUpdateEffect(spy, [value])
+ },
+ { value: 1, other: false },
+ )
+
+ expect(spy).not.toHaveBeenCalled()
+ expect(teardown).not.toHaveBeenCalled()
+
+ wrapper.setProps({ value: 2 })
+
+ expect(spy).toHaveBeenCalledTimes(1)
+ expect(teardown).not.toHaveBeenCalled()
+
+ // unrelated render
+ wrapper.setProps({ value: 2, other: true })
+
+ expect(spy).toHaveBeenCalledTimes(1)
+
+ wrapper.setProps({ value: 3, other: true })
+
+ expect(spy).toHaveBeenCalledTimes(2)
+ expect(teardown).toHaveBeenCalledTimes(1)
+ })
+})