Structuring React Gantt State with Jotai’s Atomic Model
State handling in React tends to get complicated the moment an application grows beyond a few isolated components. With project planning interfaces, the amount of interconnected UI behavior increases very quickly.
Previously, we explored several ways to organize this kind of logic, including Redux Toolkit, Zustand, MobX, and XState. Jotai approaches the problem differently. Instead of pushing everything into one global store, it breaks the state into isolated atomic units. To demonstrate how that model behaves in practice, we’ll use the react-gantt-jotai-starter project built with DHTMLX React Gantt.
Before moving into the implementation details, it’s worth understanding why Jotai feels noticeably different from more traditional React state libraries.
Understanding Jotai’s Atomic Model
Jotai was introduced as an alternative to the common useContext + useState pattern that many React teams eventually outgrow. In larger applications, context-based state often causes broader component updates than expected, and tracking why something re-rendered becomes frustrating surprisingly fast.
Jotai tries to reduce that friction by keeping the state extremely granular. The library gained traction partly because it stays very close to React’s native hooks philosophy instead of introducing a large architectural layer on top of it. That simplicity is one of the reasons many companies adopted it in production environments.
People often compare Jotai to Zustand because both libraries avoid heavy configuration and excessive boilerplate. The real distinction appears in how they organize data internally.
With Zustand, state typically lives inside a shared store that can later be divided into smaller slices. Jotai removes the idea of a mandatory central store entirely. Instead, every piece of state exists as its own atom. That architectural difference changes how updates propagate through the UI.
A component subscribes only to the atoms it actually consumes. When one atom changes, unrelated parts of the interface remain untouched. In data-heavy interfaces, this usually leads to more controlled rendering behavior and fewer optimization workarounds.
The API also stays fairly flexible. Atoms can be combined, derived from one another, or used purely for updates without exposing readable values. Because of that, the same model works both for lightweight local interactions and for more involved application flows.
Jotai also plays nicely with newer React capabilities, including Suspense and concurrent rendering patterns, which makes it feel relatively future-proof in modern React setups. The trade-off becomes noticeable later.
As applications expand, the number of atoms can grow rapidly. At that stage, organization becomes critical. Without consistent naming and structure, an atom-based architecture can become just as difficult to navigate as an oversized global store.
Why Jotai Works Naturally with React Gantt Interfaces
Interactive Gantt components behave differently from ordinary CRUD-style UIs. A seemingly small action like dragging a task along the timeline often triggers several related updates throughout the chart.
That constant chain reaction is exactly where Jotai starts to feel practical. Because updates are isolated into narrowly scoped atoms, changes remain localized. Rescheduling one task does not force unrelated parts of the application to refresh or recalculate unnecessarily. The update affects only the pieces directly connected to that interaction.
In practice, this makes state transitions easier to follow. Instead of tracing changes across reducers, middleware, or deeply nested state objects, you usually deal with a much smaller update surface. That becomes especially valuable once a project timeline grows and interactions happen continuously.
To see why this structure stays manageable, it helps to examine the overall data flow before diving into implementation details.
Organizing Data Flow Around Atoms
In this setup, the core Gantt data is stored inside a primary atom that acts as the authoritative state source for the chart. Tasks, dependency links, configuration values, and related UI state all live there.
Updates, however, are intentionally separated. Rather than mutating the main state directly, individual write-only atoms handle specific operations independently. One atom processes task creation, another updates links, another adjusts zoom configuration, and so forth.
The React Gantt component itself remains mostly passive. Its responsibility is simply to read current state values and pass them into DHTMLX React Gantt through props. When the user interacts with the chart, those interactions are routed through data.save, which delegates the update to the appropriate atom.
What’s important is that the same mechanism applies outside the chart as well. Toolbar actions such as undo, redo, or zoom changes do not introduce separate synchronization logic. They follow the identical update pipeline as interactions originating directly inside the Gantt UI.
A simplified flow looks roughly like this:
user interaction → data.save intercepts the change → matching write-only atom executes → history snapshot updates → main Gantt atom changes → React Gantt re-renders with new state
That consistency is what keeps the architecture understandable once the interface becomes more complex.
How the Integration Works in Practice
Let’s move from structure to actual behavior and look at how state updates are implemented.
1. Keeping Gantt Data Inside Atoms
The main state atom holds the full dataset, including tasks, links, and configuration (zoom, etc.):
export const ganttStateAtom = atom<GanttState>({
tasks: seedTasks,
links: seedLinks,
config: { zoom: defaultZoomLevels },
});
const maxHistory = 50;
export const pastAtom = atom<GanttState[]>([]);
export const futureAtom = atom<GanttState[]>([]);Alongside it, there are two additional atoms: pastAtom and futureAtom. These are used for undo/redo history.
To prevent unbounded memory growth, history is capped (for example, at 50 entries). Older snapshots are dropped once the limit is reached.
2. Applying Updates Through Write-Only Atoms
All modifications happen through dedicated write-only atoms. Each one represents a specific mutation: create, update, or delete.
export const addTaskAtom = atom(null, (get, set, task: SerializedTask) => {
pushHistory(get, set, get(ganttStateAtom));
set(ganttStateAtom, {
...get(ganttStateAtom),
tasks: [...get(ganttStateAtom).tasks, { ...task, id: `DB_ID:${task.id}` }],
});
return { ...task, id: `DB_ID:${task.id}` };
});Before applying any change, the current state is pushed into history. This ensures undo/redo works reliably later.
After that, the atom updates ganttStateAtom with a new immutable state.
These atoms are accessed via useSetAtom, which allows triggering updates without subscribing to their values. Only the main state atom is read by the UI.
3. Translating UI Actions into State Changes
User interactions inside DHTMLX React Gantt are not handled directly in the UI layer. Instead, everything goes through data.save. This function acts as a bridge between the visual layer and the state layer.
When a task is created, updated, or removed, the callback translates that action into the corresponding atom call. In creation flows, the updated entity (often with a generated ID) is returned to the Gantt to keep identifiers consistent.
This small detail prevents common synchronization issues between UI-generated IDs and store-generated IDs.
4. Handling Undo and Redo Outside the UI Layer
Undo/redo is implemented entirely outside the component layer. Each state change is recorded before mutation. The history stack stores full snapshots of the Gantt state.
- pastAtom holds previous states
- futureAtom holds states for redo
When undo is triggered, the last snapshot is restored and moved into the future stack. Redo performs the opposite operation.
Because snapshots capture the entire state, undo/redo remains consistent across all types of changes.
You can continue mastering integration patterns of DHTMLX React Gantt with Jotai using the official DHTMLX tutorial.
Closing Remarks
Jotai fits surprisingly well into a Gantt-based workflow. Its atomic structure keeps updates localized, which is important when working with highly interactive interfaces like DHTMLX React Gantt.
That said, it’s not a universal solution. As applications scale and the number of atoms grows, maintaining a clear structure becomes a responsibility of the developer rather than the library.
In smaller or medium-sized systems, it feels lightweight and direct. In larger enterprise setups, centralized approaches can still make more sense depending on the complexity of the shared state.
