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) + }) +})