Skip to content
Open
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
301 changes: 301 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,304 @@
# useToggles

[![useToggles](https://raw.githubusercontent.com/ava/toggles/main/public/useToggles.jpg)](https://alexcory.notion.site/toggles)

**Make toggle logic so clear, it reads like plain English.**

> *"What really matters is how well your code communicates to the people who are going to read it later."*
> — Kent C. Dodds

A React hooks library that standardizes toggle **verbs** and **states** — so your UI logic reads like spoken language.

```jsx
import useToggles, { open, toggle } from 'toggles'

const { sidebar, modal } = useToggles()
open(sidebar) // verb(noun) — reads like English
sidebar.isOpen === true // noun.state — clear, predictable
toggle(modal) // universal toggle verb
```

## Why?

<details>
<summary><b>Before & After</b></summary>

**useState approach:**

```jsx
const [isFullscreenOpen, setIsFullscreenOpen] = useState(false)
setIsFullscreenOpen(true) // open
setIsFullscreenOpen(false) // close
setIsFullscreenOpen(prev => !prev) // toggle
isFullscreenOpen && <Fullscreen />
```

**useToggles approach:**

```jsx
const { fullscreen } = useToggles()
open(fullscreen)
close(fullscreen)
toggle(fullscreen)
fullscreen.isOpen && <Fullscreen />
```

</details>

- **Self-documenting** — `open(sidebar)` is immediately meaningful, no mental parsing required
- **Consistent** — standardized verbs (`open`, `close`, `toggle`) and states (`isOpen`, `isClosed`). You only define nouns.
- **Ship faster** — when someone says "open the sidebar," your code literally says `open(sidebar)`
- **Simple & scalable** — define nouns, invoke verbs, read states. From one toggle to dozens.
- **Tiny footprint** — ~1–2 KB gzipped

## Install

```bash
yarn add toggles
```

```bash
npm i toggles
```

## Quick Start

```jsx
import useToggles, { useToggle, open, close, toggle } from 'toggles'

function App() {
const { modal, sidebar } = useToggles(false, true)
const darkMode = useToggle(false)

return (
<>
<button onClick={() => open(modal)}>Open Modal</button>
<button onClick={() => toggle(sidebar)}>Toggle Sidebar</button>
<button onClick={() => toggle(darkMode)}>Toggle Dark Mode</button>

<div className={darkMode.isOn ? 'dark' : 'light'}>
{modal.isOpen && <Modal onClose={() => close(modal)} />}
{sidebar.isVisible && <Sidebar />}
</div>
</>
)
}
```

## The Concept: `verb(noun)` + `noun.state`

Every UI toggle follows the same pattern:

1. **`verb(noun)`** — the action you'd naturally speak — `open(sidebar)`, `check(checkbox)`
2. **`noun.state`** — a boolean question — `sidebar.isOpen`, `checkbox.isChecked`

Nouns are dynamically created via destructuring (an [autovivification](https://en.wikipedia.org/wiki/Autovivification) pattern) — define as many as you need on a single line:

```jsx
const { modal, sidebar, tooltip, dropdown } = useToggles()
```

## API

### `useToggles(...initialValues)`

Creates multiple toggles. Returns a proxy object — destructure to name your nouns.

```jsx
// All default to false
const { modal, sidebar } = useToggles()

// With initial values (mapped by destructuring order)
const { checkbox, drawer, panel } = useToggles(true, false, true)
```

#### Namespaced (global) toggles

Pass a string as the first argument to share state across components:

```jsx
// In any component — same namespace = shared state
const { spellCheck } = useToggles('editor-settings', true)
```

### `useToggle(initialValue)`

Convenience hook for a single toggle.

```jsx
const darkMode = useToggle(false)
toggle(darkMode)
darkMode.isOn // true
```

### Verbs

Import individually or use the `verbs` object:

```jsx
import { open, close, toggle, show, hide } from 'toggles'
// or
import { verbs } from 'toggles'
verbs.open(sidebar)
```

**All verb pairs:**

| + Verb (on) | - Verb (off) | State (on / off) |
|---|---|---|
| `open` | `close` | `isOpen` / `isClosed` |
| `show` | `hide` | `isShown`, `isVisible` / `isHidden` |
| `turnOn` | `turnOff` | `isOn` / `isOff` |
| `check` | `uncheck` | `isChecked` / `isUnchecked` |
| `enable` | `disable` | `isEnabled` / `isDisabled` |
| `expand` | `collapse` | `isExpanded` / `isCollapsed` |
| `activate` | `deactivate` | `isActivated` / `isDeactivated` |
| `start` | `end` | `hasStarted` / `hasEnded` |
| `connect` | `disconnect` | `isConnected` / `isDisconnected` |
| `focus` | `blur` | `isFocused` / `isBlurred` |
| `mount` | `unmount` | `isMounted` |
| `reveal` | `conceal` | `isRevealed` / `isConcealed` |
| `display` | `dismiss` | — |
| `lock` | `unlock` | `isLocked` / `isUnlocked` |
| `subscribe` | `unsubscribe` | `isSubscribed` |
| `toggle` | *(universal)* | flips current value |

### Noun States

Every noun exposes multiple boolean state properties — all derived from the same underlying value:

```jsx
const { modal } = useToggles(true)

// All positive states → true
modal.isOpen // true
modal.isVisible // true
modal.isOn // true
modal.isChecked // true
modal.isEnabled // true
modal.isExpanded // true
modal.isActive // true

// All negative states → false (inverse)
modal.isClosed // false
modal.isHidden // false
modal.isOff // false
modal.isDisabled // false
modal.isCollapsed // false
```

Use whichever state reads best in context. A modal uses `isOpen`, a checkbox uses `isChecked`, dark mode uses `isOn` — same underlying boolean, different semantic lens.

## Examples

### Modal

```jsx
import useToggles, { open, close } from 'toggles'

function ConfirmDialog() {
const { confirmModal } = useToggles()

return (
<>
<button onClick={() => open(confirmModal)}>Delete Item</button>

{confirmModal.isOpen && (
<Modal>
<p>Are you sure?</p>
<button onClick={() => close(confirmModal)}>Cancel</button>
<button onClick={handleDelete}>Delete</button>
</Modal>
)}
</>
)
}
```

### Settings Panel

```jsx
import useToggles, { toggle } from 'toggles'

function Settings() {
const { advanced, notifications, darkMode } = useToggles(false, true, false)

return (
<div>
<Switch
checked={notifications.isChecked}
onChange={() => toggle(notifications)}
/>
<Switch
checked={darkMode.isOn}
onChange={() => toggle(darkMode)}
/>
{advanced.isVisible && <AdvancedSettings />}
</div>
)
}
```

### Global State with Namespaces

```jsx
import useToggles, { toggle } from 'toggles'

// Header component
function Header() {
const { sidebar } = useToggles('app')
return <button onClick={() => toggle(sidebar)}>Menu</button>
}

// Layout component — shared state, no prop drilling
function Layout() {
const { sidebar } = useToggles('app')
return (
<div className={sidebar.isOpen ? 'shifted' : ''}>
{sidebar.isVisible && <Sidebar />}
<main>...</main>
</div>
)
}
```

### With useEffect

```jsx
import useToggles, { open } from 'toggles'

function DelayedOpen() {
const { settings } = useToggles(false)

useEffect(() => {
const timer = setTimeout(() => open(settings), 2000)
return () => clearTimeout(timer)
}, [settings])

return <p>Settings: {settings.isOpen ? 'OPEN' : 'CLOSED'}</p>
}
```

## Compatibility

- React 16.9+
- Works with SSR (Next.js, Remix)
- TypeScript supported
- ~1–2 KB gzipped

## Philosophy

Code should be as human-readable as possible. `useToggles` reduces the cognitive overhead of toggle state by aligning it with natural language patterns — `open(sidebar)` instead of `setIsSidebarOpen(true)`.

> *"Any fool can write code that a computer can understand. Good programmers write code that humans can understand."*
> — Martin Fowler

Read the full design philosophy at [alexcory.notion.site/toggles](https://alexcory.notion.site/toggles).

## Contributing

Issues and PRs welcome on [GitHub](https://github.com/ava/toggles).

## License

MIT