Global Dialogs Slice Pattern
Centralized Redux state for global dialog open/close, avoiding one slice per dialog.
When to use
Use this pattern for dialogs that are:
- Mounted at the app root (e.g. in
Default.tsx) - Triggered from distant parts of the component tree (e.g. a settings button opening a dialog rendered at root level)
Do NOT use this for dialogs that are local to a single component or screen -- prefer a simple useState instead.
Architecture
- One shared slice:
renderer/reducers/dialogs.tsmanages all global dialog states via aRecord<DialogId, boolean> - Single source of truth:
DIALOG_IDSruntime array defines all IDs;DialogIdtype is derived from it via(typeof DIALOG_IDS)[number] - Feature wrappers: Each feature creates a thin
*Dialog.tsfile that re-exports typed helpers bound to its ID - Single mount point:
mvvm/features/GlobalDialogs/index.tsxrenders all global dialog components at the app root
Adding a new global dialog
Step 1: Register the dialog ID
Add the new ID to the DIALOG_IDS array in renderer/reducers/dialogs.ts. The DialogId type updates automatically:
export const DIALOG_IDS = ["RELEASE_NOTES", "MY_NEW_DIALOG"] as const;
// DialogId is now "RELEASE_NOTES" | "MY_NEW_DIALOG"
Step 2: Create the feature dialog file
Create myFeature/myNewDialog.ts in the feature folder:
import {
openDialog,
closeDialog,
selectIsDialogOpen,
type DialogId,
} from "~/renderer/reducers/dialogs";
import type { State } from "~/renderer/reducers";
const DIALOG_ID: DialogId = "MY_NEW_DIALOG";
export const openMyNewDialog = () => openDialog(DIALOG_ID);
export const closeMyNewDialog = () => closeDialog(DIALOG_ID);
export const selectIsMyNewDialogOpen = (state: State) => selectIsDialogOpen(state, DIALOG_ID);
Step 3: Use in the view model
import { selectIsMyNewDialogOpen, closeMyNewDialog } from "../myNewDialog";
const useMyNewDialogViewModel = () => {
const isOpen = useSelector(selectIsMyNewDialogOpen);
const dispatch = useDispatch();
const => dispatch(closeMyNewDialog()), [dispatch]);
return { isOpen, onClose };
};
Step 4: Mount in GlobalDialogs
Add the dialog component to mvvm/features/GlobalDialogs/index.tsx:
import MyNewDialog from "LLD/features/MyFeature";
const GlobalDialogs = () => (
<>
<ModularDialogRoot />
<SendFlowRoot />
<ReleaseNotes />
<MyNewDialog />
</>
);
Step 5: Trigger from anywhere
import { openMyNewDialog } from "LLD/features/MyFeature/myNewDialog";
dispatch(openMyNewDialog());
Rules
- NEVER create a dedicated Redux slice just for dialog open/close state
- NEVER use this pattern for dialogs local to a single component -- use
useStateinstead - ALWAYS add new dialog IDs to the
DIALOG_IDSarray (the type is derived automatically) - ALWAYS use SCREAMING_SNAKE_CASE for dialog IDs
- ALWAYS mount new global dialogs in
GlobalDialogs/index.tsx, not directly inDefault.tsx - Feature dialog files should only contain the ID constant and re-exported typed helpers
- No modification to
reducers/index.tsorDefault.tsxis needed when adding new dialogs
Reference
- Slice:
apps/ledger-live-desktop/src/renderer/reducers/dialogs.ts - Mount point:
apps/ledger-live-desktop/src/mvvm/features/GlobalDialogs/index.tsx - Example:
apps/ledger-live-desktop/src/mvvm/features/ReleaseNotes/releaseNotesDialog.ts