475

useControllableState

Hook that manages both controlled and uncontrolled state patterns

statemediumtest coverage

Preferences

Manage how the app behaves.

Get notified about important updates

Use dark theme across the app

Save changes automatically as you type

Help us improve by sharing usage data

Receive product news and offers

Independent toggle — managed inside the component

import type { ComponentProps } from 'react';

import { useControllableState } from '@siberiacancode/reactuse';
import { useState } from 'react';

interface SwitchProps extends Omit<ComponentProps<'input'>, 'onChange' | 'value'> {
  initialValue?: boolean;
  value?: boolean;
  onChange?: (value: boolean) => void;
}

const Switch = ({ value, initialValue, onChange, ...props }: SwitchProps) => {
  const [checked, setChecked] = useControllableState({
    value,
    initialValue: initialValue ?? false,
    onChange
  });

  return (
    <input
      {...props}
      checked={checked ?? false}
      role='switch'
      type='checkbox'
      onChange={(event) => setChecked(event.target.checked)}
    />
  );
};

interface Preferences {
  analytics: boolean;
  autoSave: boolean;
  darkMode: boolean;
  marketing: boolean;
  notifications: boolean;
}

const SETTINGS = [
  {
    key: 'notifications',
    label: 'Notifications',
    description: 'Get notified about important updates'
  },
  {
    key: 'darkMode',
    label: 'Dark mode',
    description: 'Use dark theme across the app'
  },
  {
    key: 'autoSave',
    label: 'Auto-save',
    description: 'Save changes automatically as you type'
  },
  {
    key: 'analytics',
    label: 'Analytics',
    description: 'Help us improve by sharing usage data'
  },
  {
    key: 'marketing',
    label: 'Marketing emails',
    description: 'Receive product news and offers'
  }
] as const;

const ALL_OFF = {
  notifications: false,
  darkMode: false,
  autoSave: false,
  analytics: false,
  marketing: false
} as const;

const ALL_ON: Preferences = {
  notifications: true,
  darkMode: true,
  autoSave: true,
  analytics: true,
  marketing: true
};

const RECOMMENDED: Preferences = {
  notifications: true,
  darkMode: true,
  autoSave: true,
  analytics: true,
  marketing: false
};

const Demo = () => {
  const [preferences, setPreferences] = useState<Preferences>(ALL_OFF);

  return (
    <section className='flex min-w-sm flex-col gap-6 md:min-w-md'>
      <div className='flex flex-col gap-2'>
        <h3>Preferences</h3>
        <p className='text-muted-foreground'>Manage how the app behaves.</p>
      </div>

      <div className='flex flex-wrap items-center gap-2'>
        <button data-variant='outline' type='button' onClick={() => setPreferences(ALL_ON)}>
          All on
        </button>
        <button data-variant='outline' type='button' onClick={() => setPreferences(ALL_OFF)}>
          All off
        </button>
        <button data-variant='outline' type='button' onClick={() => setPreferences(RECOMMENDED)}>
          Recommended
        </button>
      </div>

      <div className='flex flex-col'>
        {SETTINGS.map((setting) => (
          <div key={setting.key} className='flex items-center justify-between gap-4 py-3'>
            <div className='flex flex-col gap-1'>
              <label htmlFor={setting.key}>{setting.label}</label>
              <p className='text-muted-foreground text-xs'>{setting.description}</p>
            </div>

            <Switch
              id={setting.key}
              value={preferences[setting.key]}
              onChange={(value) =>
                setPreferences((current) => ({ ...current, [setting.key]: value }))
              }
            />
          </div>
        ))}

        <div className='border-border mt-2 border-t pt-2'>
          <div className='flex items-center justify-between gap-4 py-3'>
            <div className='flex flex-col gap-1'>
              <label htmlFor='beta'>Beta features</label>
              <p className='text-muted-foreground text-xs'>
                Independent toggle — managed inside the component
              </p>
            </div>

            <Switch id='beta' initialValue={false} />
          </div>
        </div>
      </div>
    </section>
  );
};

export default Demo;

Installation

pnpm add @siberiacancode/reactuse

Usage

const [value, setValue, isControlled] = useControllableState({ initialValue: 'initial' });

Type Declarations

export interface UseControllableStateOptions<Value> {
  /** The initial value for uncontrolled state */
  initialValue?: Value;
  /** The controlled value */
  value?: Value;
  /** The onChange callback */
  onChange?: (value: Value) => void;
}

export type UseControllableStateReturn<Value> = [
  /** Current value */
  value: Value,
  /** Setter function that works with both controlled and uncontrolled state */
  setValue: (nextValue: ((prevValue: Value) => Value) | Value) => void,
  /** Whether the state is controlled */
  isControlled: boolean
];

API

Parameters

NameTypeDefaultNote
options.valueValue-The controlled value. When provided, the component becomes controlled
options.initialValueValue-The initial value for uncontrolled state
options.onChange(value: Value) => void-The callback function called when the state changes

Returns

UseControllableStateReturn<Value>

Contributors

ddebabin

Last updated on