@codeleap/hooks 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@codeleap/hooks",
3
+ "version": "4.3.0",
4
+ "main": "src/index.ts",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "url": "https://github.com/codeleap-uk/internal-libs-monorepo.git",
8
+ "type": "git",
9
+ "directory": "packages/hooks"
10
+ },
11
+ "devDependencies": {
12
+ "@codeleap/config": "4.3.0",
13
+ "@codeleap/types": "4.3.0",
14
+ "@codeleap/utils": "4.3.0",
15
+ "@codeleap/logger": "4.3.0",
16
+ "ts-node-dev": "1.1.8"
17
+ },
18
+ "scripts": {
19
+ "build": "echo 'No build needed'"
20
+ },
21
+ "peerDependencies": {
22
+ "@codeleap/types": "4.3.0",
23
+ "@codeleap/utils": "4.3.0",
24
+ "@codeleap/logger": "4.3.0",
25
+ "axios": "^1.7.9",
26
+ "typescript": "5.0.4",
27
+ "react": "18.1.0",
28
+ "@tanstack/react-query": "5.60.6"
29
+ }
30
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@codeleap/hooks",
3
+ "version": "4.3.0",
4
+ "main": "src/index.ts",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "url": "https://github.com/codeleap-uk/internal-libs-monorepo.git",
8
+ "type": "git",
9
+ "directory": "packages/hooks"
10
+ },
11
+ "devDependencies": {
12
+ "@codeleap/config": "workspace:*",
13
+ "@codeleap/types": "workspace:*",
14
+ "@codeleap/utils": "workspace:*",
15
+ "@codeleap/logger": "workspace:*",
16
+ "ts-node-dev": "1.1.8"
17
+ },
18
+ "scripts": {
19
+ "build": "echo 'No build needed'"
20
+ },
21
+ "peerDependencies": {
22
+ "@codeleap/types": "workspace:*",
23
+ "@codeleap/utils": "workspace:*",
24
+ "@codeleap/logger": "workspace:*",
25
+ "axios": "^1.7.9",
26
+ "typescript": "5.0.4",
27
+ "react": "18.1.0",
28
+ "@tanstack/react-query": "5.60.6"
29
+ }
30
+ }
@@ -0,0 +1,51 @@
1
+ import React, { createContext, ReactNode, useContext } from 'react'
2
+ import { Logger, silentLogger } from '@codeleap/logger'
3
+ import { AppSettings } from '@codeleap/types'
4
+
5
+ type ContextProps = {
6
+ logger: Logger
7
+ Settings: AppSettings
8
+ isBrowser: boolean
9
+ }
10
+
11
+ const GlobalContext = createContext<ContextProps>({} as ContextProps)
12
+
13
+ type ProviderProps = {
14
+ logger: Logger
15
+ settings: AppSettings
16
+ isBrowser: boolean
17
+ children: ReactNode
18
+ }
19
+
20
+ export const GlobalContextProvider = (props: ProviderProps) => {
21
+ const {
22
+ logger,
23
+ settings,
24
+ isBrowser,
25
+ children
26
+ } = props
27
+
28
+ const value: ContextProps = {
29
+ logger: logger || silentLogger,
30
+ Settings: settings,
31
+ isBrowser,
32
+ }
33
+
34
+ return (
35
+ <GlobalContext.Provider
36
+ value={value}
37
+ >
38
+ {children}
39
+ </GlobalContext.Provider>
40
+ )
41
+ }
42
+
43
+ export const useGlobalContext = () => {
44
+ const context = useContext(GlobalContext)
45
+
46
+ if (!context) {
47
+ throw new Error('GlobalContext not found')
48
+ }
49
+
50
+ return context
51
+ }
package/src/index.ts ADDED
@@ -0,0 +1,47 @@
1
+ import {
2
+ useEffect,
3
+ useRef,
4
+ useState,
5
+ useImperativeHandle,
6
+ useMemo,
7
+ useCallback,
8
+ useContext,
9
+ useLayoutEffect,
10
+ useDebugValue,
11
+ useReducer,
12
+ } from 'react'
13
+
14
+ export * from './useConditionalState'
15
+ export * from './usePromise'
16
+ export * from './useListState'
17
+ export * from './useUncontrolled'
18
+ export * from './useCounter'
19
+ export * from './useForceRender'
20
+ export * from './useDebounce'
21
+ export * from './useInterval'
22
+ export * from './onMount'
23
+ export * from './onUpdate'
24
+ export * from './usePrevious'
25
+ export * from './useToggle'
26
+ export * from './useBooleanToggle'
27
+ export * from './useModal'
28
+ export * from './usePlaces'
29
+ export * from './usePlacesAutocompleteUtils'
30
+ export * from './useEffectOnce'
31
+ export * from './useUnmount'
32
+ export * from './useSearch'
33
+ export * from './usePartialState'
34
+ export * from './GlobalContext'
35
+
36
+ export {
37
+ useEffect,
38
+ useRef,
39
+ useState,
40
+ useImperativeHandle,
41
+ useMemo,
42
+ useCallback,
43
+ useContext,
44
+ useLayoutEffect,
45
+ useDebugValue,
46
+ useReducer,
47
+ }
package/src/onMount.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { useEffect } from 'react'
2
+ import { AnyFunction } from '@codeleap/types'
3
+
4
+ export const onMount = (func: AnyFunction) => {
5
+ useEffect(() => {
6
+ return func()
7
+ }, [])
8
+ }
@@ -0,0 +1,8 @@
1
+ import { useEffect } from 'react'
2
+ import { AnyFunction } from '@codeleap/types'
3
+
4
+ export const onUpdate = (func: AnyFunction, listeners = []) => {
5
+ useEffect(() => {
6
+ return func()
7
+ }, listeners)
8
+ }
@@ -0,0 +1,15 @@
1
+ import { useState } from 'react'
2
+
3
+ export function useBooleanToggle(initial: boolean) {
4
+ const [v, setV] = useState(initial)
5
+
6
+ function toggleOrSet(value?: boolean) {
7
+ if (typeof value === 'boolean') {
8
+ setV(value)
9
+ } else {
10
+ setV((previous) => !previous)
11
+ }
12
+ }
13
+
14
+ return [v, toggleOrSet] as const
15
+ }
@@ -0,0 +1,26 @@
1
+ import { useState, SetStateAction, Dispatch } from 'react'
2
+ import { AnyFunction } from '@codeleap/types'
3
+ import { useBooleanToggle } from './useBooleanToggle'
4
+
5
+ type UseConditionalStateOptions<T> = {
6
+ initialValue?: T
7
+ isBooleanToggle?: boolean
8
+ }
9
+
10
+ type SetState<T> = Dispatch<SetStateAction<T>> & ((value: T) => void) & (() => void)
11
+
12
+ export const useConditionalState = <T>(
13
+ value: T | undefined,
14
+ setter: AnyFunction,
15
+ options: UseConditionalStateOptions<T> = {}
16
+ ): [T, SetState<T>] => {
17
+ const state = options?.isBooleanToggle
18
+ ? useBooleanToggle(options?.initialValue as boolean)
19
+ : useState(options?.initialValue)
20
+
21
+ if (!value && typeof setter === 'function') {
22
+ return [value, setter]
23
+ }
24
+
25
+ return state as unknown as [T, SetState<T>]
26
+ }
@@ -0,0 +1,5 @@
1
+ import { useReducer } from 'react'
2
+
3
+ export function useCounter() {
4
+ return useReducer((x) => x + 1, 0)
5
+ }
@@ -0,0 +1,25 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+
3
+ export function useDebounce<T>(
4
+ value: T,
5
+ debounce: number,
6
+ ): [T, () => void] {
7
+ const [debouncedValue, setDebouncedValue] = useState(value)
8
+
9
+ const timeoutRef = useRef(null)
10
+
11
+ const reset = () => {
12
+ if (timeoutRef.current) {
13
+ clearTimeout(timeoutRef.current)
14
+ }
15
+ }
16
+ useEffect(() => {
17
+ timeoutRef.current = setTimeout(() => {
18
+ setDebouncedValue(value)
19
+ }, debounce)
20
+
21
+ return reset
22
+ }, [value])
23
+
24
+ return [debouncedValue, reset]
25
+ }
@@ -0,0 +1,5 @@
1
+ import { EffectCallback, useEffect } from 'react'
2
+
3
+ export const useEffectOnce = (effect: EffectCallback) => {
4
+ useEffect(effect, [])
5
+ }
@@ -0,0 +1,6 @@
1
+ import { useReducer } from 'react'
2
+
3
+ export function useForceRender() {
4
+ const [_, forceRender] = useReducer((x) => x + 1, 0)
5
+ return forceRender
6
+ }
@@ -0,0 +1,21 @@
1
+ import { useRef } from 'react'
2
+ import { AnyFunction } from '@codeleap/types'
3
+
4
+ export function useInterval(callback: AnyFunction, interval: number) {
5
+ const intervalRef = useRef(null)
6
+
7
+ function clear() {
8
+ clearInterval(intervalRef.current)
9
+ intervalRef.current = null
10
+ }
11
+
12
+ function start() {
13
+ intervalRef.current = setInterval(callback, interval)
14
+ }
15
+
16
+ return {
17
+ clear,
18
+ start,
19
+ interval: intervalRef.current,
20
+ }
21
+ }
@@ -0,0 +1,92 @@
1
+ import { useState } from 'react'
2
+
3
+ export interface UseListStateHandler<T> {
4
+ setState: React.Dispatch<React.SetStateAction<T[]>>
5
+ append: (...items: T[]) => void
6
+ prepend: (...items: T[]) => void
7
+ insert: (index: number, ...items: T[]) => void
8
+ pop: () => void
9
+ shift: () => void
10
+ apply: (fn: (item: T, index?: number) => T) => void
11
+ applyWhere: (
12
+ condition: (item: T, index: number) => boolean,
13
+ fn: (item: T, index?: number) => T
14
+ ) => void
15
+ remove: (...indices: number[]) => void
16
+ reorder: ({ from, to }: { from: number; to: number }) => void
17
+ setItem: (index: number, item: T) => void
18
+ setItemProp: <K extends keyof T, U extends T[K]>(index: number, prop: K, value: U) => void
19
+ }
20
+
21
+ export type UseListState<T> = [T[], UseListStateHandler<T>]
22
+
23
+ export function useListState<T>(initialValue: (T[] | (() => T[])) = []): UseListState<T> {
24
+ const [state, setState] = useState(initialValue)
25
+
26
+ const append = (...items: T[]) => setState((current) => [...current, ...items])
27
+ const prepend = (...items: T[]) => setState((current) => [...items, ...current])
28
+
29
+ const insert = (index: number, ...items: T[]) => setState((current) => [...current.slice(0, index), ...items, ...current.slice(index)])
30
+
31
+ const apply = (fn: (item: T, index?: number) => T) => setState((current) => current.map((item, index) => fn(item, index)))
32
+
33
+ const remove = (...indices: number[]) => setState((current) => current.filter((_, index) => !indices.includes(index)))
34
+
35
+ const pop = () => setState((current) => {
36
+ const cloned = [...current]
37
+ cloned.pop()
38
+ return cloned
39
+ })
40
+
41
+ const shift = () => setState((current) => {
42
+ const cloned = [...current]
43
+ cloned.shift()
44
+ return cloned
45
+ })
46
+
47
+ const reorder = ({ from, to }: { from: number; to: number }) => setState((current) => {
48
+ const cloned = [...current]
49
+ const item = current[from]
50
+
51
+ cloned.splice(from, 1)
52
+ cloned.splice(to, 0, item)
53
+
54
+ return cloned
55
+ })
56
+
57
+ const setItem = (index: number, item: T) => setState((current) => {
58
+ const cloned = [...current]
59
+ cloned[index] = item
60
+ return cloned
61
+ })
62
+
63
+ const setItemProp = <K extends keyof T, U extends T[K]>(index: number, prop: K, value: U) => setState((current) => {
64
+ const cloned = [...current]
65
+ cloned[index] = { ...cloned[index], [prop]: value }
66
+ return cloned
67
+ })
68
+
69
+ const applyWhere = (
70
+ condition: (item: T, index: number) => boolean,
71
+ fn: (item: T, index?: number) => T,
72
+ ) => setState((current) => current.map((item, index) => (condition(item, index) ? fn(item, index) : item)),
73
+ )
74
+
75
+ return [
76
+ state,
77
+ {
78
+ setState,
79
+ append,
80
+ prepend,
81
+ insert,
82
+ pop,
83
+ shift,
84
+ apply,
85
+ applyWhere,
86
+ remove,
87
+ reorder,
88
+ setItem,
89
+ setItemProp,
90
+ },
91
+ ]
92
+ }
@@ -0,0 +1,10 @@
1
+ import { useBooleanToggle } from './useBooleanToggle'
2
+
3
+ export function useModal(startsOpen = false) {
4
+ const [visible, toggle] = useBooleanToggle(startsOpen)
5
+
6
+ return {
7
+ visible,
8
+ toggle,
9
+ }
10
+ }
@@ -0,0 +1,21 @@
1
+ import { useState } from 'react'
2
+ import { DeepPartial } from '@codeleap/types'
3
+ import { deepMerge } from '@codeleap/utils'
4
+
5
+ type SetPartialStateCallback<T> = (value: T) => DeepPartial<T>
6
+
7
+ export function usePartialState<T= any>(initial: T | (() => T)) {
8
+ const [state, setState] = useState(initial)
9
+
10
+ function setPartial(
11
+ value: DeepPartial<T> | SetPartialStateCallback<T>,
12
+ ) {
13
+ if (typeof value === 'function') {
14
+ setState((v) => deepMerge(v, value(v as T)))
15
+ } else {
16
+ setState(deepMerge(state, value))
17
+ }
18
+ }
19
+
20
+ return [state as T, setPartial] as const
21
+ }
@@ -0,0 +1,63 @@
1
+ import axios from 'axios'
2
+ import { useQuery } from '@tanstack/react-query'
3
+
4
+ const BASE_URL = 'https://maps.googleapis.com/maps/api/place/autocomplete/json'
5
+ const BASE_URL_DETAILS = 'https://maps.googleapis.com/maps/api/place/details/json'
6
+ const BASE_URL_GEOCODING = 'https://maps.googleapis.com/maps/api/geocode/json'
7
+
8
+ const latLngRegex = /^-?\d+(\.\d+)?,-?\d+(\.\d+)?$/
9
+
10
+ type Params = {
11
+ input?: string
12
+ key?: string
13
+ showDetails?: boolean
14
+ }
15
+
16
+ export const retrievePlaceDetails = async (placeId: string, apiKey: string) => {
17
+ const response = await axios.get(BASE_URL_DETAILS, {
18
+ params: {
19
+ place_id: placeId,
20
+ key: apiKey,
21
+ },
22
+ })
23
+
24
+ return response?.data?.result
25
+ }
26
+
27
+ export const retrievePlaces = async (params: Params) => {
28
+ let response
29
+ const inputWithoutSpaces = params?.input?.replace(/\s/g, '')
30
+ const isLatLng = latLngRegex?.test(inputWithoutSpaces)
31
+
32
+ if (isLatLng) {
33
+ response = await axios?.get(BASE_URL_GEOCODING, { params: { latlng: params?.input, key: params?.key } })
34
+ } else {
35
+ response = await axios?.get(BASE_URL, { params })
36
+ }
37
+
38
+ let places = response?.data?.results || response?.data?.predictions
39
+
40
+ if (params?.showDetails) {
41
+ const apiKey = params?.key
42
+ places = await Promise.all(
43
+ places?.map(async (place) => {
44
+ const placeId = place?.place_id
45
+ const details = await retrievePlaceDetails(placeId, apiKey)
46
+ return { ...place, details }
47
+ }),
48
+ )
49
+ }
50
+
51
+ return places
52
+ }
53
+
54
+ export type UsePlacesParams = Params
55
+
56
+ export const usePlaces = (params: Params) => {
57
+ const places = useQuery({
58
+ queryKey: ['places', params],
59
+ queryFn: () => retrievePlaces(params),
60
+ })
61
+
62
+ return places
63
+ }
@@ -0,0 +1,59 @@
1
+ import React from 'react'
2
+ import { TypeGuards } from '@codeleap/types'
3
+
4
+ export type UsePlacesAutocompleteUtilsProps<T extends Record<string, any>> = {
5
+ debounce?: number
6
+ onValueChange?: (address: string) => void
7
+ onPress?: (address: string, item: T) => void
8
+ }
9
+
10
+ export const usePlacesAutocompleteUtils = <T extends Record<string, any>>(props: UsePlacesAutocompleteUtilsProps<T>) => {
11
+ const {
12
+ debounce = 250,
13
+ onValueChange,
14
+ onPress,
15
+ } = props
16
+
17
+ const [address, setAddress] = React.useState('')
18
+ const [isTyping, setIsTyping] = React.useState(false)
19
+
20
+ const setSearchTimeout = React.useRef<NodeJS.Timeout | null>(null)
21
+
22
+ const handleChangeAddress = (address: string) => {
23
+ setAddress(address)
24
+
25
+ if (TypeGuards.isNil(debounce)) {
26
+ onValueChange?.(address)
27
+ setTimeout(() => setIsTyping(false), 250)
28
+ } else {
29
+ if (setSearchTimeout.current) {
30
+ clearTimeout(setSearchTimeout.current)
31
+ setSearchTimeout.current = null
32
+ }
33
+
34
+ setSearchTimeout.current = setTimeout(() => {
35
+ onValueChange?.(address)
36
+ setIsTyping(false)
37
+ }, debounce ?? 0)
38
+ }
39
+ }
40
+
41
+ const handlePressAddress = (address: string, item: T) => {
42
+ setAddress(address)
43
+ onPress?.(address, item)
44
+ }
45
+
46
+ const handleClearAddress = () => {
47
+ setAddress('')
48
+ onValueChange?.('')
49
+ }
50
+
51
+ return {
52
+ handleChangeAddress,
53
+ handlePressAddress,
54
+ handleClearAddress,
55
+ address,
56
+ isTyping,
57
+ setIsTyping,
58
+ }
59
+ }
@@ -0,0 +1,9 @@
1
+ import { useEffect, useRef } from 'react'
2
+
3
+ export const usePrevious = <T>(value: T) => {
4
+ const ref = useRef<T>()
5
+ useEffect(() => {
6
+ ref.current = value
7
+ })
8
+ return ref.current
9
+ }
@@ -0,0 +1,46 @@
1
+ import { useRef } from 'react'
2
+ import { TypeGuards, AnyFunction } from '@codeleap/types'
3
+
4
+ type UsePromiseOptions<T = any> = {
5
+ onResolve?: (value: T) => void
6
+ onReject?: (err: any) => void
7
+ timeout?: number
8
+ debugName?: string
9
+ }
10
+
11
+ export const usePromise = <T = any>(options?: UsePromiseOptions<T>) => {
12
+ const rejectRef = useRef<AnyFunction>()
13
+ const resolveRef = useRef<(v:T) => any>()
14
+ const timeoutRef = useRef(null)
15
+ const reject = async (err: any) => {
16
+ await rejectRef.current?.(err)
17
+ options?.onReject?.(err)
18
+ if (timeoutRef.current) clearTimeout(timeoutRef.current)
19
+ rejectRef.current = null
20
+ }
21
+
22
+ const resolve = async (value: T) => {
23
+ await resolveRef.current?.(value)
24
+ options?.onResolve?.(value)
25
+ if (timeoutRef.current) clearTimeout(timeoutRef.current)
26
+ resolveRef.current = null
27
+ }
28
+
29
+ const _await = () => {
30
+ return new Promise<T>((resolve, reject) => {
31
+ rejectRef.current = reject
32
+ resolveRef.current = resolve
33
+ if (TypeGuards.isNumber(options?.timeout) && options?.timeout > 0) {
34
+ timeoutRef.current = setTimeout(() => {
35
+ reject(new Error(`usePromise: ${options?.debugName || ''} timed out after ${options?.timeout}ms`))
36
+ }, options?.timeout)
37
+ }
38
+ })
39
+ }
40
+
41
+ return {
42
+ _await,
43
+ resolve,
44
+ reject,
45
+ }
46
+ }
@@ -0,0 +1,71 @@
1
+ import { useState } from 'react'
2
+ import { useBooleanToggle } from '../useBooleanToggle'
3
+ import { useSearchParams } from './types'
4
+ import { TypeGuards } from '@codeleap/types'
5
+
6
+ export function useSearch<T extends string|number = string, Multi extends boolean = false>(props: useSearchParams<T, Multi>) {
7
+ const {
8
+ value,
9
+ multiple,
10
+ options,
11
+ filterItems,
12
+ debugName,
13
+ defaultOptions,
14
+
15
+ loadOptions,
16
+ onLoadOptionsError,
17
+ } = { ...props }
18
+
19
+ const [loading, setLoading] = useBooleanToggle(false)
20
+ const isValueArray = TypeGuards.isArray(value) && multiple
21
+ const [labelOptions, setLabelOptions] = useState(() => {
22
+ if (isValueArray) {
23
+ return defaultOptions.filter(o => value.includes(o.value))
24
+ }
25
+
26
+ const _option = defaultOptions.find(o => o.value === value)
27
+
28
+ if (!_option) {
29
+ return []
30
+ }
31
+
32
+ return [_option]
33
+ })
34
+
35
+ const [filteredOptions, setFilteredOptions] = useState(defaultOptions)
36
+ const [, setSearchInput] = useState('')
37
+
38
+ async function load() {
39
+ setLoading(true)
40
+ try {
41
+ const options = await loadOptions('')
42
+ setFilteredOptions(options)
43
+ } catch (e) {
44
+ onLoadOptionsError(e)
45
+ }
46
+ setLoading(false)
47
+ }
48
+
49
+ const onChangeSearch = async (searchValue:string) => {
50
+ setSearchInput(searchValue)
51
+
52
+ if (!!loadOptions) {
53
+ setLoading(true)
54
+ try {
55
+ const _opts = await loadOptions(searchValue)
56
+ setFilteredOptions(_opts)
57
+ } catch (e) {
58
+ console.error(`Error loading select options [${debugName}], e`)
59
+ onLoadOptionsError?.(e)
60
+ }
61
+ setTimeout(() => {
62
+ setLoading(false)
63
+ }, 0)
64
+ return
65
+ }
66
+ const _opts = filterItems(searchValue, options)
67
+ setFilteredOptions(_opts)
68
+ }
69
+
70
+ return { loading, setLoading, labelOptions, setLabelOptions, filteredOptions, load, onChangeSearch }
71
+ }
@@ -0,0 +1,14 @@
1
+ export type AutocompleteValue<T, Multi extends boolean = false> = Multi extends true ? T[] : T
2
+
3
+ type Options<T> = { label: any; value: T }[]
4
+
5
+ export type useSearchParams<T, Multi extends boolean = false> = {
6
+ value: AutocompleteValue<T, Multi>
7
+ multiple:Multi
8
+ options: Options<T>
9
+ filterItems: (search: string, items: Options<T>) => Options<T>
10
+ debugName: string
11
+ defaultOptions: Options<T>
12
+ loadOptions: (search: string) => Promise<Options<T>>
13
+ onLoadOptionsError: (error: any) => void
14
+ }
@@ -0,0 +1,16 @@
1
+ import { useState } from 'react'
2
+
3
+ export function useToggle<T extends readonly [any, any], V extends T[0] | T[1]>(
4
+ options: T,
5
+ initial: V,
6
+ ) {
7
+ const [value, setValue] = useState(initial)
8
+
9
+ function toggleOrSetValue(newValue?: V) {
10
+ const v: V = newValue || (value === options[0] ? options[1] : options[0])
11
+
12
+ setValue(v)
13
+ }
14
+
15
+ return [value, toggleOrSetValue] as const
16
+ }
@@ -0,0 +1,69 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+
3
+ export type UncontrolledMode = 'initial' | 'controlled' | 'uncontrolled'
4
+
5
+ export interface UncontrolledOptions<T> {
6
+ value: T | null | undefined
7
+ defaultValue: T | null | undefined
8
+ finalValue: T | null
9
+ onChange(value: T | null): void
10
+ onValueUpdate?(value: T | null): void
11
+ rule: (value: T | null | undefined) => boolean
12
+ }
13
+
14
+ export function useUncontrolled<T>({
15
+ value,
16
+ defaultValue,
17
+ finalValue,
18
+ rule,
19
+ onChange,
20
+ onValueUpdate,
21
+ }: UncontrolledOptions<T>): readonly [T | null, (nextValue: T | null) => void, UncontrolledMode] {
22
+ // determine, whether new props indicate controlled state
23
+ const shouldBeControlled = rule(value)
24
+
25
+ // initialize state
26
+ const modeRef = useRef<UncontrolledMode>('initial')
27
+ const initialValue = rule(defaultValue) ? defaultValue : finalValue
28
+ const [uncontrolledValue, setUncontrolledValue] = useState(initialValue)
29
+
30
+ // compute effective value
31
+ let effectiveValue = shouldBeControlled ? value : uncontrolledValue
32
+
33
+ if (!shouldBeControlled && modeRef.current === 'controlled') {
34
+ // We are transitioning from controlled to uncontrolled
35
+ // this transition is special as it happens when clearing out
36
+ // the input using "invalid" value (typically null or undefined).
37
+ //
38
+ // Since the value is invalid, doing nothing would mean just
39
+ // transitioning to uncontrolled state and using whatever value
40
+ // it currently holds which is likely not the behavior
41
+ // user expects, so lets change the state to finalValue.
42
+ //
43
+ // The value will be propagated to internal state by useEffect below.
44
+
45
+ effectiveValue = finalValue
46
+ }
47
+
48
+ modeRef.current = shouldBeControlled ? 'controlled' : 'uncontrolled'
49
+ const mode = modeRef.current
50
+
51
+ const handleChange = (nextValue: T | null) => {
52
+ typeof onChange === 'function' && onChange(nextValue)
53
+
54
+ // Controlled input only triggers onChange event and expects
55
+ // the controller to propagate new value back.
56
+ if (mode === 'uncontrolled') {
57
+ setUncontrolledValue(nextValue)
58
+ }
59
+ }
60
+
61
+ useEffect(() => {
62
+ if (mode === 'uncontrolled') {
63
+ setUncontrolledValue(effectiveValue)
64
+ }
65
+ typeof onValueUpdate === 'function' && onValueUpdate(effectiveValue)
66
+ }, [mode, effectiveValue])
67
+
68
+ return [effectiveValue, handleChange, modeRef.current] as const
69
+ }
@@ -0,0 +1,11 @@
1
+ import { useRef } from 'react'
2
+ import { useEffectOnce } from './useEffectOnce'
3
+
4
+ export const useUnmount = (fn: () => any): void => {
5
+ const fnRef = useRef(fn)
6
+
7
+ // update the ref each render so if it change the newest callback will be invoked
8
+ fnRef.current = fn
9
+
10
+ useEffectOnce(() => () => fnRef.current())
11
+ }