Skip to main content

Overview

Stores a value together with validation metadata. The hook keeps the current value, tracks whether it is valid, and preserves the last value that passed validation.

Import

import { useValidatedState } from '@kuzenbo/hooks';

Signature

function useValidatedState<T>(
  initialValue: T,
  validate: (value: T) => boolean,
  initialValidationState?: boolean
): UseValidatedStateReturnValue<T>;

Parameters

initialValue
T
required
Initial value used to seed state
validate
(value: T) => boolean
required
Validation function used for every update
initialValidationState
boolean
Optional initial validity override

Return Value

[state, setValue]
UseValidatedStateReturnValue<T>
A tuple containing the validation state object and setter function
state
UseValidatedStateValue<T>
Object containing current value and validation metadata
state.value
T
Current value
state.lastValidValue
T | undefined
Last value that passed validation
state.valid
boolean
True if the current value is valid, false otherwise
setValue
(value: T) => void
Setter function that updates value and runs validation

Usage

Email Input Validation

import { useValidatedState } from '@kuzenbo/hooks';

function EmailInput() {
  const [{ value, valid, lastValidValue }, setValue] = useValidatedState(
    '',
    (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)
  );

  return (
    <div>
      <input
        type="email"
        value={value}
        onChange={(e) => setValue(e.target.value)}
        className={!valid && value ? 'invalid' : ''}
      />
      {!valid && value && <span className="error">Invalid email</span>}
      {lastValidValue && (
        <p>Last valid email: {lastValidValue}</p>
      )}
    </div>
  );
}

Numeric Range Validation

import { useValidatedState } from '@kuzenbo/hooks';

function AgeInput() {
  const [{ value, valid }, setValue] = useValidatedState(
    0,
    (age) => age >= 18 && age <= 120,
    false
  );

  return (
    <div>
      <input
        type="number"
        value={value}
        onChange={(e) => setValue(parseInt(e.target.value) || 0)}
      />
      {!valid && (
        <span className="error">Age must be between 18 and 120</span>
      )}
      <button disabled={!valid}>Submit</button>
    </div>
  );
}

Password Strength Validation

import { useValidatedState } from '@kuzenbo/hooks';

function PasswordInput() {
  const validatePassword = (password: string) => {
    return (
      password.length >= 8 &&
      /[A-Z]/.test(password) &&
      /[a-z]/.test(password) &&
      /[0-9]/.test(password)
    );
  };

  const [{ value, valid }, setValue] = useValidatedState(
    '',
    validatePassword
  );

  return (
    <div>
      <input
        type="password"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <div className="password-requirements">
        <p className={value.length >= 8 ? 'valid' : 'invalid'}>
          At least 8 characters
        </p>
        <p className={/[A-Z]/.test(value) ? 'valid' : 'invalid'}>
          Contains uppercase letter
        </p>
        <p className={/[a-z]/.test(value) ? 'valid' : 'invalid'}>
          Contains lowercase letter
        </p>
        <p className={/[0-9]/.test(value) ? 'valid' : 'invalid'}>
          Contains number
        </p>
      </div>
      <p>Password is {valid ? 'strong' : 'weak'}</p>
    </div>
  );
}

Form Field with Fallback

import { useValidatedState } from '@kuzenbo/hooks';

function URLInput() {
  const validateURL = (url: string) => {
    try {
      new URL(url);
      return true;
    } catch {
      return false;
    }
  };

  const [{ value, valid, lastValidValue }, setValue] = useValidatedState(
    'https://example.com',
    validateURL
  );

  const handleSubmit = () => {
    // Use the last valid value if current is invalid
    const urlToSubmit = valid ? value : lastValidValue;
    console.log('Submitting:', urlToSubmit);
  };

  return (
    <div>
      <input
        type="url"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      {!valid && <span className="error">Invalid URL</span>}
      <button onClick={handleSubmit}>
        Submit {!valid && '(will use last valid URL)'}
      </button>
    </div>
  );
}

Complex Object Validation

import { useValidatedState } from '@kuzenbo/hooks';

interface UserProfile {
  username: string;
  bio: string;
  age: number;
}

function ProfileEditor() {
  const validateProfile = (profile: UserProfile) => {
    return (
      profile.username.length >= 3 &&
      profile.bio.length <= 200 &&
      profile.age >= 13
    );
  };

  const [{ value, valid, lastValidValue }, setValue] = useValidatedState<UserProfile>(
    { username: '', bio: '', age: 0 },
    validateProfile,
    false
  );

  const updateField = <K extends keyof UserProfile>(
    field: K,
    fieldValue: UserProfile[K]
  ) => {
    setValue({ ...value, [field]: fieldValue });
  };

  return (
    <div>
      <input
        type="text"
        value={value.username}
        onChange={(e) => updateField('username', e.target.value)}
        placeholder="Username (min 3 chars)"
      />
      <textarea
        value={value.bio}
        onChange={(e) => updateField('bio', e.target.value)}
        placeholder="Bio (max 200 chars)"
        maxLength={200}
      />
      <input
        type="number"
        value={value.age}
        onChange={(e) => updateField('age', parseInt(e.target.value) || 0)}
        placeholder="Age (min 13)"
      />
      
      <div className="validation-status">
        <p>Profile is {valid ? 'valid' : 'invalid'}</p>
        {lastValidValue && !valid && (
          <button onClick={() => setValue(lastValidValue)}>
            Restore Last Valid State
          </button>
        )}
      </div>
      
      <button disabled={!valid}>Save Profile</button>
    </div>
  );
}

Type Definitions

export interface UseValidatedStateValue<T> {
  /** Current value */
  value: T;

  /** Last valid value */
  lastValidValue: T | undefined;

  /** True if the current value is valid, false otherwise */
  valid: boolean;
}

export type UseValidatedStateReturnValue<T> = [
  /** Current value */
  UseValidatedStateValue<T>,
  /** Handler to update the state, passes `value` and `payload` to `onChange` */
  (value: T) => void,
];

Build docs developers (and LLMs) love