State Management with Zustand
Relevant source files
The following files were used as context for generating this wiki page:The Character Sheet Maker application utilizes Zustand, a small, fast, and scalable state-management solution. It provides a centralized store, useSheetStore, to manage the entire state of the character sheet, including its content, layout, and styling. This approach ensures a single source of truth and simplifies state synchronization across various UI components.
A key feature of the implementation is the use of Zustand's persist middleware. This automatically saves the entire sheet state to the browser's localStorage, providing a "Local-First" experience where a user's work is preserved between sessions without needing a backend. The state is stored under the key rpg-sheet-storage.
Sources: src/store/useSheetStore.ts:2, 53-104, package.json:26, README.md:9
Store Architecture
The core of the state management is the useSheetStore hook, created using Zustand's create function. This store encapsulates the state's data structures and the actions that can modify them.
Sources: src/store/useSheetStore.ts:53
State Structure
The application's global state is defined by the SheetState interface. It is composed of three main parts: blocks, globalStyle, and isCompact.
The diagram below illustrates the main data structures within the Zustand store.
Sources: src/store/useSheetStore.ts:6-51
Block Interface
Each element on the character sheet canvas is represented by a Block object. These objects contain information about their position, size, type, and content. The i property is a unique identifier required by the react-grid-layout library.
| Property | Type | Description |
|---|---|---|
i | string | Unique identifier for the block, required by React Grid Layout (RGL). |
x | number | The x-coordinate of the block on the grid. |
y | number | The y-coordinate of the block on the grid. |
w | number | The width of the block in grid units. |
h | number | The height of the block in grid units. |
type | "text" | "image" | ... | The type of content the block represents (e.g., header, form-box). |
data | any | The actual content of the block, such as text or an image URL. |
config | object | Configuration specific to the block type, like level for a header or shapeType for a shape. |
Sources: src/store/useSheetStore.ts:19-33
SheetStyle Interface
The globalStyle object in the store holds styling information that applies to the entire sheet, allowing for consistent theming.
| Property | Type | Description |
|---|---|---|
theme | ThemeKey | The key of the currently active theme (e.g., "simple", "medieval"). |
fonts | object | An object containing font names for different text elements (h1, h2, h3, body). |
Sources: src/store/useSheetStore.ts:8-17, src/lib/themes.ts:1
State Persistence
The store is wrapped with the persist middleware to automatically save and rehydrate the state from localStorage. This makes the user's design persistent across browser sessions.
// src/store/useSheetStore.ts:53-104
export const useSheetStore = create<SheetState>()(
persist(
(set) => ({
// ... state and actions
}),
{ name: "rpg-sheet-storage" } // AutoSave in localStorage
)
);
Sources: src/store/useSheetStore.ts:54, 103
State Actions
Actions are functions within the store that are used to update the state. Components call these actions to report user interactions or other events.
| Action | Parameters | Description |
|---|---|---|
addBlock | type: Block["type"], config?: Block["config"] | Adds a new block of a specified type to the blocks array. |
removeBlock | id: string | Removes a block from the blocks array using its unique ID. |
updateBlockData | id: string, data: any | Updates the data property of a specific block. |
updateLayout | layout: any[] | Synchronizes the position and dimensions (x, y, w, h) of blocks from react-grid-layout back to the store. |
setTheme | theme: ThemeKey | Changes the global theme for the sheet. |
setGlobalFont | tag: keyof SheetStyle["fonts"], fontName: string | Sets a specific font for a given text element type (e.g., h1). |
toggleCompact | (none) | Toggles the isCompact boolean, which controls the grid's compaction behavior. |
Sources: src/store/useSheetStore.ts:35-45, 71-101
Data Flow and Component Interaction
React components interact with the useSheetStore hook to both read state and dispatch actions.
Reading State
Components can subscribe to state changes by calling the useSheetStore hook and selecting the necessary state slices.
- The
Canvascomponent readsblocksto render them andisCompactto configure the grid layout. - The
ToolsSidebarreadsisCompactandglobalStyleto display the current state of UI controls. - The
BlockRendererreadsglobalStyleto apply the correct theme classes to each block.
Sources: src/components/sheet/Canvas.tsx:11, src/components/layout/ToolsSidebar.tsx:13, src/components/sheet/BlockRenderer.tsx:6
Updating State
Components trigger state updates by calling the action functions returned by the useSheetStore hook.
The following diagram shows the sequence of events when a user adds a new block from the ToolsSidebar.
Another common interaction is updating a block's content, handled by BlockRenderer.
This flow shows how BlockRenderer uses updateBlockData to modify the content of a specific block.
Sources: src/components/sheet/BlockRenderer.tsx:5, 30, 214, src/store/useSheetStore.ts:99-101
Finally, when a user moves or resizes a block on the grid, react-grid-layout triggers the onLayoutChange event, which is handled by the Canvas component to persist the new layout.
// src/components/sheet/Canvas.tsx:43-44
<ResponsiveGridLayout
// ...
onLayoutChange={(currentLayout) => updateLayout(currentLayout)}
// ...
>
The updateLayout action then synchronizes these changes back into the blocks array in the Zustand store.
Sources: src/components/sheet/Canvas.tsx:44, src/store/useSheetStore.ts:93-98
Summary
The project's state management is built around a single Zustand store, useSheetStore. This store is the definitive source for all data related to the character sheet's blocks, layout, and global styling. By leveraging Zustand's persist middleware, the application provides a seamless, local-first user experience. Components interact with the store in a decoupled manner, reading state and dispatching actions, which creates a predictable and maintainable data flow throughout the application.