Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/useCallbackRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { useState } from 'react'
*
* return <div ref={attachRef} />
* ```
*
* @category refs
*/
export default function useCallbackRef<TValue = unknown>(): [
TValue | null,
Expand Down
2 changes: 2 additions & 0 deletions src/useImmediateUpdateEffect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import useStableMemo from './useStableMemo'
* setValue(value)
* }, [value])
* ```
*
* @category effects
*/
function useImmediateUpdateEffect(effect: () => void, deps: DependencyList) {
const firstRef = useRef(true)
Expand Down
2 changes: 2 additions & 0 deletions src/useIsomorphicEffect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions src/useMergedRefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function mergeRefs<T>(refA?: Ref<T> | null, refB?: Ref<T> | null) {
*
* @param refA A Callback or mutable Ref
* @param refB A Callback or mutable Ref
* @category refs
*/
function useMergedRefs<T>(refA?: Ref<T> | null, refB?: Ref<T> | null) {
return useMemo(() => mergeRefs(refA, refB), [refA, refB])
Expand Down
24 changes: 24 additions & 0 deletions src/useMountEffect.ts
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions src/useRefWithInitialValueFactory.ts
Original file line number Diff line number Diff line change
@@ -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<ExpensiveValue>(() => constructExpensiveValue())
*
* ```
*
* @param initialValueFactory A factory function returning the ref's default value
* @category refs
*/
export default function useRefWithInitialValueFactory<T>(
initialValueFactory: () => T,
) {
const ref = useRef<T>(dft)
if (ref.current === dft) {
ref.current = initialValueFactory()
}
return ref
}
34 changes: 34 additions & 0 deletions src/useUpdateEffect.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLInput>(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
1 change: 1 addition & 0 deletions src/useUpdatedRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(value: T) {
const valueRef = useRef<T>(value)
Expand Down
1 change: 1 addition & 0 deletions src/useWillUnmount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion test/useImmediateUpdateEffect.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
30 changes: 30 additions & 0 deletions test/useMountEffect.test.tsx
Original file line number Diff line number Diff line change
@@ -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)
})
})
25 changes: 25 additions & 0 deletions test/useRefWithInitialValueFactory.test.tsx
Original file line number Diff line number Diff line change
@@ -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)
})
})
34 changes: 34 additions & 0 deletions test/useUpdateEffect.test.tsx
Original file line number Diff line number Diff line change
@@ -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)
})
})