A pattern for triggering dialogs from anywhere without prop drilling — and why atomic state is the missing link between your table actions and your modals.

The Problem

Imagine a standard administration dashboard. You have a UserTable component that displays a list of users, and a UserCreateDialog that handles the form for adding a new one.

The most straightforward way to connect them is to host the open state in a common parent:

// ❌ The "State Bucket" Anti-pattern function UserManagementPage() { const [isDialogOpen, setIsDialogOpen] = useState(false); return ( <div> <UserTable onAddUser={() => setIsDialogOpen(true)} /> <UserCreateDialog open={isDialogOpen} onOpenChange={setIsDialogOpen} /> </div> ); } 

This looks correct at first glance, but as the application grows, three problems emerge:

  1. The Render Tax: Every time you open or close the dialog, the entire UserManagementPage re-renders, including the expensive UserTable.
  2. Prop Drilling: If the "Add User" button is nested inside a TableToolbar, which is inside UserTable, you have to pass onAddUser through three layers of components that don't care about it.
  3. Component Bloat: Your "Page" component stops being a layout and starts being a bucket for UI state that belongs to specific modals.

You might try to fix this by putting the dialog inside the UserTable component, but now your table — a component meant for data display — is also responsible for form validation, API calls, and dialog lifecycles.

The real tension is that the trigger (the button) and the response (the dialog) are semantically linked but architecturally distant.

Atomic state management solves this by pulling the "visibility" state out of the component tree and into a stable, external reference.

// ✅ The Atomic Solution import { Atom, useAtom } from "@effect-atom/atom-react"; // State exists outside the tree export const openCreateDialogAtom = Atom.make(false); export function UserCreateDialog() { const [open, setOpen] = useAtom(openCreateDialogAtom); return <Dialog open={open} onOpenChange={setOpen}>...</Dialog>; } 

The Mental Model

Think of this pattern as a Panic Button.

In a traditional house, a light switch is hard-wired to a specific light fixture. If you want to move the switch to another room, you have to rip out the walls and re-wire everything.

In an atomic setup, the switch is a wireless remote (the useAtomSet hook) and the light is a smart bulb (the Dialog component). They aren't physically connected. Instead, they both listen to the same radio frequency (the Atom).

  • Identity: The Atom is the frequency. It stays the same regardless of who is listening.
  • Update Timing: When you press the remote, the frequency updates instantly, but only the smart bulb reacts.
  • Read Timing: The bulb only checks the frequency when it needs to decide whether to stay on or turn off.

The UI tree is for layout; the Atomic state is for orchestration.

How to Use It

1. Setup

First, ensure you have the atomic library installed. We use @effect-atom/atom-react for its tight integration with the Effect ecosystem and React's concurrency features.

src/ └── features/ └── user/ ├── state.ts ├── UserCreateDialog.tsx └── UserManagementTable.tsx 

state.ts

import { Atom } from "@effect-atom/atom-react"; // Use pipe(Atom.keepAlive) if you need the state to persist // even when no components are mounted. export const openUserCreateAtom = Atom.make(false).pipe(Atom.keepAlive); 

2. The Global Toggle Pattern

The most common use case is a simple open/close toggle for a creation form.

// UserCreateDialog.tsx import { useAtom } from "@effect-atom/atom-react"; import { openUserCreateAtom } from "./state"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; export function UserCreateDialog() { const [open, setOpen] = useAtom(openUserCreateAtom); return ( <Dialog open={open} onOpenChange={setOpen}> <DialogContent> <DialogHeader> <DialogTitle>Create New User</DialogTitle> </DialogHeader> {/* Form content here */} </DialogContent> </Dialog> ); } 

3. The Distant Trigger

Now, any component — no matter how deep in the tree — can open this dialog without receiving props.

// UserManagementTable.tsx import { useAtomSet } from "@effect-atom/atom-react"; import { openUserCreateAtom } from "./state"; import { Button } from "@/components/ui/button"; import { UserCreateDialog } from "./UserCreateDialog"; export function UserManagementTable() { const setOpen = useAtomSet(openUserCreateAtom); return ( <div> <div className="flex justify-between mb-4"> <h1 className="text-2xl font-bold">Users</h1> <Button onClick={() => setOpen(true)}>Add User</Button> </div> <Table>...</Table> {/* The dialog is rendered here for accessibility, but its state is controlled externally */} <UserCreateDialog /> </div> ); } 

4. Passing Contextual Data

Sometimes you need to open a dialog and pass it an ID (e.g., for an "Edit" dialog). You can store the object or ID in the atom itself.

// state.ts export const editUserAtom = Atom.make<string | null>(null); // EditTrigger.tsx const setEditId = useAtomSet(editUserAtom); return <Button onClick={() => setEditId(user.id)}>Edit</Button>; // EditDialog.tsx const [editId, setEditId] = useAtom(editUserAtom); const isOpen = editId !== null; const handleClose = () => setEditId(null); 

❌ Anti-pattern: Using Atoms for Local-Only State

Do not use an Atom if the state is only used within a single component and its immediate children. If a Tooltip is only triggered by one Icon, useState is still the correct choice. Using an Atom here adds unnecessary complexity and makes the component harder to reuse in isolation.

When to Use It

Reach for Atomic dialog management when all of the following are true:

  • The trigger (Button) and the recipient (Dialog) are more than two levels apart in the component tree.
  • The Dialog is a "Global" or "Feature-Level" singleton (e.g., "Create User", "Command Palette").
  • You want to avoid unnecessary re-renders of large lists or tables when a modal opens.
Situation Better Tool
Simple Tooltip useState
Multi-step Wizard useReducer or a Local Context
Shared User Prefs Context API (Stable, rarely changes)
Large List Performance Atomic State
Deeply Nested Triggers Atomic State
Form Input State react-hook-form

How It Works Under the Hood

Atomic state libraries (like @effect-atom or Jotai) work by bypassing the React "render-to-reconcile" loop for state distribution.

  1. Stable Identity: The Atom object is a plain JavaScript reference created outside the render cycle. Its memory address never changes.
  2. The Registry: The library maintains an internal map of "subscribers." When useAtom is called, the component registers itself to that specific Atom's key.
  3. Selective Updates: When setOpen(true) is called, the library looks up only the components subscribed to that specific Atom and triggers a targeted re-render for those components only.
  4. Bypassing the Parent: Because the update is pushed directly to the Dialog component, the parent Table or Page never re-renders.

The Render/Commit Timeline

Phase Table Component Atom State Dialog Component
Initial Rendered false Hidden
User Clicks No Change true (Trigger Update)
Commit No Change true Rendered (Visible)
Form Submit No Change false Unmounted/Hidden

A Note on SSR: Since atoms are stored outside the component tree, they can lead to hydration mismatches if not handled carefully. Always ensure your atoms are initialized with a default value that matches the server's initial render (usually false for dialogs).

Conclusion

Atomic state management turns the "visibility" of your UI into a first-class citizen. By decoupling the trigger from the layout, you create components that are easier to test, more performant, and significantly more maintainable.

  • Decouple triggers from dialogs to kill prop drilling.
  • Protect parent components from unnecessary re-renders.
  • Centralize orchestration logic without bloating your UI components.

The next time you find yourself passing a setIsOpen function through a Table component just to reach a toolbar, stop. Make an Atom instead.