'use client'
/**
* useInput Hook
*
* Manages controlled input state for text fields, textareas, and other text-based inputs.
* Handles value state, change events, blur events, and integration with validation.
*
* @example
* const { value, onChange, onBlur, setValue, reset } = useInput('', {
* onValidate: (v) => v.trim().length > 0
* })
*
*
*/
import { ChangeEvent, FocusEvent, useCallback, useState } from 'react'
interface UseInputOptions {
/** Initial value for the input */
initialValue?: string
/** Callback when input changes */
onChange?: (value: string) => void
/** Callback when input blurs */
onBlur?: (value: string) => void
/** Validation function - returns error string or empty string */
onValidate?: (value: string) => string
/** Trim whitespace on change */
trim?: boolean
/** Transform value on change */
transform?: (value: string) => string
}
interface UseInputState {
value: string
isDirty: boolean
isTouched: boolean
error: string
isValid: boolean
}
interface UseInputHandlers {
onChange: (e: ChangeEvent) => void
onBlur: (e: FocusEvent) => void
setValue: (value: string) => void
setError: (error: string) => void
clearError: () => void
reset: () => void
touch: () => void
validate: () => boolean
}
interface UseInputReturn extends UseInputState {
handlers: UseInputHandlers
}
/**
* Hook for managing controlled input state
*
* @param initialValue Initial value
* @param options Configuration options
* @returns Input state and handlers
*/
export function useInput(initialValue: string = '', options?: UseInputOptions): UseInputReturn {
const [value, setValue] = useState(initialValue)
const [isDirty, setIsDirty] = useState(false)
const [isTouched, setIsTouched] = useState(false)
const [error, setError] = useState('')
// Determine if input is valid (no error and has changed from initial)
const isValid = !error && isDirty
/**
* Handle input change event
*/
const handleChange = useCallback(
(e: ChangeEvent) => {
let newValue = e.target.value
// Apply transformations
if (options?.trim) {
newValue = newValue.trim()
}
if (options?.transform) {
newValue = options.transform(newValue)
}
setValue(newValue)
setIsDirty(newValue !== initialValue)
// Clear error when user starts typing
if (error) {
setError('')
}
// Call user's onChange callback
options?.onChange?.(newValue)
},
[initialValue, error, options]
)
/**
* Handle input blur event
*/
const handleBlur = useCallback(
(e: FocusEvent) => {
setIsTouched(true)
// Validate on blur if validator provided
if (options?.onValidate) {
const validationError = options.onValidate(value)
if (validationError) {
setError(validationError)
}
}
// Call user's onBlur callback
options?.onBlur?.(value)
},
[value, options]
)
/**
* Set value programmatically
*/
const setProgrammaticValue = useCallback((newValue: string) => {
setValue(newValue)
setIsDirty(newValue !== initialValue)
}, [initialValue])
/**
* Set error message
*/
const setErrorMessage = useCallback((errorMsg: string) => {
setError(errorMsg)
}, [])
/**
* Clear error message
*/
const clearErrorMessage = useCallback(() => {
setError('')
}, [])
/**
* Reset to initial state
*/
const reset = useCallback(() => {
setValue(initialValue)
setIsDirty(false)
setIsTouched(false)
setError('')
}, [initialValue])
/**
* Mark input as touched
*/
const touch = useCallback(() => {
setIsTouched(true)
}, [])
/**
* Manually validate current value
*/
const validate = useCallback((): boolean => {
if (options?.onValidate) {
const validationError = options.onValidate(value)
if (validationError) {
setError(validationError)
return false
}
}
clearErrorMessage()
return true
}, [value, options])
return {
value,
isDirty,
isTouched,
error,
isValid,
handlers: {
onChange: handleChange,
onBlur: handleBlur,
setValue: setProgrammaticValue,
setError: setErrorMessage,
clearError: clearErrorMessage,
reset,
touch,
validate,
},
}
}
export default useInput