{"id":15871,"date":"2026-09-08T10:10:21","date_gmt":"2026-09-08T07:10:21","guid":{"rendered":"https:\/\/dhtmlx.com\/blog\/?p=15871"},"modified":"2026-09-08T10:59:41","modified_gmt":"2026-09-08T07:59:41","slug":"managing-server-state-in-react-gantt-chart-with-tanstack-query-and-zustand","status":"publish","type":"post","link":"https:\/\/dhtmlx.com\/blog\/managing-server-state-in-react-gantt-chart-with-tanstack-query-and-zustand\/","title":{"rendered":"Managing Server State in React Gantt Chart with TanStack Query and Zustand"},"content":{"rendered":"<p>In one of our previous tutorials, we covered the basics of <a href=\"https:\/\/dhtmlx.com\/blog\/using-zustand-state-management-apps-dhtmlx-react-gantt-scheduler\/\" rel=\"noopener\" target=\"_blank\">state management in DHTMLX React Gantt with Zustand<\/a>. There, we focused on managing Gantt data on the client side. However, real project planning apps, in most cases, need to load tasks and dependency links from the server and save user edits there, which calls for a dedicated server-state layer. This is where TanStack Query comes into play.<\/p>\n<p>In this post, we&#8217;ll explain how TanStack Query complements Zustand in the <a href=\"https:\/\/dhtmlx.com\/docs\/products\/dhtmlxGantt-for-React\/\" rel=\"noopener\" target=\"_blank\">DHTMLX React Gantt component<\/a> and highlight the key integration steps. As reference sources of information, we&#8217;ll use the GitHub repository of the <a href=\"https:\/\/github.com\/DHTMLX\/react-gantt-tanstack-query-starter\" rel=\"noopener\" target=\"_blank\">starter project<\/a> and the official <a href=\"https:\/\/docs.dhtmlx.com\/gantt\/integrations\/react\/state\/tanstack-query\/\" rel=\"noopener\" target=\"_blank\">TanStack Query integration guide<\/a>.<\/p>\n<h2>Introduction to TanStack Query and Its Approach to Handling Server State in React<\/h2>\n<p>Before the TanStack Query release in late 2019, developers used various approaches for managing server state: built-in React hooks (useEffect, useState), client-side state managers (Redux, MobX, Zustand, etc.), or custom caching abstractions. These techniques can be effective to some extent, but they require considerable manual effort from developers and may lead to additional boilerplate and complexity. Another serious difficulty related to server state is that it can quickly become out of date, as server data may change independently of the client. TanStack Query is designed to address these well-known <a href=\"https:\/\/2025.stateofreact.com\/en-US\/libraries\/state-management\/#state_management_pain_points\" rel=\"noopener\" target=\"_blank\">pain points<\/a> in state management. It provides a clear mental model for fetching, caching, mutating, and synchronizing server data in React apps. <\/p>\n<p>In our starter demo, TanStack Query and Zustand form a two-layer architecture with a clear split of responsibilities:<\/p>\n<table>\n<tbody>\n<tr>\n<td><strong>Layer<\/strong><\/td>\n<td><strong>Responsible for the state of<\/strong><\/td>\n<td><strong>Role in the DHTMLX starter demo<\/strong><\/td>\n<\/tr>\n<tr>\n<td>TanStack Query<\/td>\n<td>Tasks, links<\/td>\n<td>Manages server data fetched from and synchronized with the server <\/td>\n<\/tr>\n<tr>\n<td>Zustand<\/td>\n<td>Zoom config, undo\/redo history<\/td>\n<td>Manages local UI state that does not require server synchronization<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Before we move to integration details, let us elaborate on the main TanStack Query mechanisms required for server state workflow:<\/p>\n<table>\n<tbody>\n<tr>\n<td><strong>TanStack API item<\/strong><\/td>\n<td><strong>Main function<\/strong><\/td>\n<td><strong>Role in the DHTMLX starter demo<\/strong><\/td>\n<\/tr>\n<tr>\n<td><strong>useQuery<\/strong><\/td>\n<td>Reading server data<\/td>\n<td>Fetching and caching Gantt data  (tasks+links)<\/td>\n<\/tr>\n<tr>\n<td><strong>useMutation<\/strong><\/td>\n<td>Modifying server data<\/td>\n<td>Performing CRUD operations with tasks and links via API<\/td>\n<\/tr>\n<tr>\n<td><strong>queryClient<\/strong><\/td>\n<td>Working with the query cache<\/td>\n<td>providing access to cached Gantt data and query operations<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Together, these API items organize the core data flow: <strong>useQuery<\/strong> loads tasks and links into the cache, <strong>useMutation<\/strong> sends user changes to the backend, and <strong>queryClient<\/strong> invalidates or updates the cache as needed.<\/p>\n<p>Now, it is high time to take a closer look at how these pieces come together in the React Gantt + TanStack Query starter.<\/p>\n<h2>Bringing TanStack Query and Zustand Together in DHTMLX React Gantt<\/h2>\n<p>With the responsibilities of each layer clear, let&#8217;s see how they come together in code. We\u2019ll put aside some general configurations already encountered in the Zustand-only approach, and dwell only on what is specific to TanStack Query.<\/p>\n<p>Before we start, it is worth mentioning that our starter uses a minimal Express backend with JSON file storage to keep things simple and focus on the client-side integration. Swapping it out in a production application is straightforward, since the mutations work the same way regardless of what sits behind the API.<\/p>\n<h3>Setting Up the Query Client<\/h3>\n<p>Every hook from the library needs access to a <strong>QueryClient<\/strong> instance. You create it once at the application root, outside the render function, and pass it to a <strong>QueryClientProvider<\/strong> that wraps the app. In this case, the client is reachable from anywhere in the tree.<\/p>\n<div class=\"codecolorer-container javascript default\" style=\"overflow:auto;white-space:nowrap;\"><div class=\"javascript codecolorer\"><span class=\"kw5\">import<\/span> <span class=\"br0\">&#123;<\/span> QueryClient<span class=\"sy0\">,<\/span> QueryClientProvider <span class=\"br0\">&#125;<\/span> from <span class=\"st0\">'@tanstack\/react-query'<\/span><span class=\"sy0\">;<\/span><br \/>\n<br \/>\n<span class=\"kw1\">const<\/span> client <span class=\"sy0\">=<\/span> <span class=\"kw1\">new<\/span> QueryClient<span class=\"br0\">&#40;<\/span><span class=\"br0\">&#41;<\/span><span class=\"sy0\">;<\/span><br \/>\n<br \/>\ncreateRoot<span class=\"br0\">&#40;<\/span>document.<span class=\"me1\">getElementById<\/span><span class=\"br0\">&#40;<\/span><span class=\"st0\">'root'<\/span><span class=\"br0\">&#41;<\/span><span class=\"sy0\">!<\/span><span class=\"br0\">&#41;<\/span>.<span class=\"me1\">render<\/span><span class=\"br0\">&#40;<\/span><br \/>\n&nbsp; <span class=\"sy0\">&lt;<\/span>StrictMode<span class=\"sy0\">&gt;<\/span><br \/>\n&nbsp; &nbsp; <span class=\"sy0\">&lt;<\/span>QueryClientProvider client<span class=\"sy0\">=<\/span><span class=\"br0\">&#123;<\/span>client<span class=\"br0\">&#125;<\/span><span class=\"sy0\">&gt;<\/span><br \/>\n&nbsp; &nbsp; &nbsp; <span class=\"sy0\">&lt;<\/span>App <span class=\"sy0\">\/&gt;<\/span><br \/>\n&nbsp; &nbsp; <span class=\"sy0\">&lt;\/<\/span>QueryClientProvider<span class=\"sy0\">&gt;<\/span><br \/>\n&nbsp; <span class=\"sy0\">&lt;\/<\/span>StrictMode<span class=\"sy0\">&gt;,<\/span><br \/>\n<span class=\"br0\">&#41;<\/span><span class=\"sy0\">;<\/span><\/div><\/div>\n<p>The client sits at module scope rather than inside App for a reason: creating a new QueryClient on every render would discard the existing cache. Every component inside the provider then shares the same instance.<\/p>\n<h3>Fetching Gantt Data with useQuery<\/h3>\n<p>In the starter&#8217;s environment, the <strong>useQuery<\/strong> hook serves as the single source of truth for the chart&#8217;s data layer. One call of this hook pulls the whole dataset on mount:<\/p>\n<div class=\"codecolorer-container javascript default\" style=\"overflow:auto;white-space:nowrap;\"><div class=\"javascript codecolorer\"><span class=\"kw1\">const<\/span> <span class=\"br0\">&#123;<\/span><br \/>\n&nbsp; data<span class=\"sy0\">:<\/span> fetchedData<span class=\"sy0\">,<\/span><br \/>\n&nbsp; isLoading<span class=\"sy0\">,<\/span><br \/>\n&nbsp; isError<span class=\"sy0\">,<\/span><br \/>\n&nbsp; error<span class=\"sy0\">,<\/span><br \/>\n<span class=\"br0\">&#125;<\/span> <span class=\"sy0\">=<\/span> useQuery<span class=\"sy0\">&lt;<\/span><span class=\"br0\">&#123;<\/span> tasks<span class=\"sy0\">:<\/span> SerializedTask<span class=\"br0\">&#91;<\/span><span class=\"br0\">&#93;<\/span><span class=\"sy0\">;<\/span> links<span class=\"sy0\">:<\/span> Link<span class=\"br0\">&#91;<\/span><span class=\"br0\">&#93;<\/span> <span class=\"br0\">&#125;<\/span><span class=\"sy0\">&gt;<\/span><span class=\"br0\">&#40;<\/span><span class=\"br0\">&#123;<\/span> queryKey<span class=\"sy0\">:<\/span> <span class=\"br0\">&#91;<\/span><span class=\"st0\">'data'<\/span><span class=\"br0\">&#93;<\/span><span class=\"sy0\">,<\/span> queryFn<span class=\"sy0\">:<\/span> fetchData <span class=\"br0\">&#125;<\/span><span class=\"br0\">&#41;<\/span><span class=\"sy0\">;<\/span><br \/>\n<br \/>\n<span class=\"kw1\">const<\/span> <span class=\"br0\">&#123;<\/span> tasks<span class=\"sy0\">,<\/span> links <span class=\"br0\">&#125;<\/span> <span class=\"sy0\">=<\/span> fetchedData <span class=\"sy0\">||<\/span> <span class=\"br0\">&#123;<\/span> tasks<span class=\"sy0\">:<\/span> <span class=\"br0\">&#91;<\/span><span class=\"br0\">&#93;<\/span><span class=\"sy0\">,<\/span> links<span class=\"sy0\">:<\/span> <span class=\"br0\">&#91;<\/span><span class=\"br0\">&#93;<\/span> <span class=\"br0\">&#125;<\/span><span class=\"sy0\">;<\/span><\/div><\/div>\n<p>The <strong>isLoading<\/strong> and <strong>isError<\/strong> values allow you to show a loading indicator or an error message before the chart is rendered. Returning early on either one keeps <strong>ReactGantt<\/strong> from ever seeing undefined data, and it prevents an empty Gantt from flashing while the request is in flight. The fallback to empty arrays keeps the destructuring safe while <strong>fetchedData<\/strong> is still undefined. It gives <strong>makeSnapshot()<\/strong> (the undo\/redo helper covered below) real arrays to clone before the first response arrives. <\/p>\n<p>Keeping two data collections (tasks and links) under one <strong>[&#8216;data&#8217;]<\/strong> key is a deliberate simplification. One request brings both, and links never outrun their tasks. In this case, invalidating the key reloads everything. <\/p>\n<h3>Passing Dates Between the Server and Gantt<\/h3>\n<p>JSON has no date type, so <strong>Date<\/strong> objects arrive from the server as plain strings. Anyone connecting the chart to a backend hits this. Starting from v9.1.3, React Gantt parses ISO strings automatically, but the starter spells the conversion out through templates. You still need them on earlier versions, or when your API does not use ISO 8601:<\/p>\n<div class=\"codecolorer-container javascript default\" style=\"overflow:auto;white-space:nowrap;\"><div class=\"javascript codecolorer\"><span class=\"kw1\">const<\/span> templates<span class=\"sy0\">:<\/span> ReactGanttProps<span class=\"br0\">&#91;<\/span><span class=\"st0\">'templates'<\/span><span class=\"br0\">&#93;<\/span> <span class=\"sy0\">=<\/span> useMemo<span class=\"br0\">&#40;<\/span><br \/>\n&nbsp; <span class=\"br0\">&#40;<\/span><span class=\"br0\">&#41;<\/span> <span class=\"sy0\">=&gt;<\/span> <span class=\"br0\">&#40;<\/span><span class=\"br0\">&#123;<\/span><br \/>\n&nbsp; &nbsp; format_date<span class=\"sy0\">:<\/span> <span class=\"br0\">&#40;<\/span>d<span class=\"br0\">&#41;<\/span> <span class=\"sy0\">=&gt;<\/span> d.<span class=\"me1\">toISOString<\/span><span class=\"br0\">&#40;<\/span><span class=\"br0\">&#41;<\/span><span class=\"sy0\">,<\/span><br \/>\n&nbsp; &nbsp; parse_date<span class=\"sy0\">:<\/span> <span class=\"br0\">&#40;<\/span>s<span class=\"br0\">&#41;<\/span> <span class=\"sy0\">=&gt;<\/span> <span class=\"kw1\">new<\/span> <span class=\"kw4\">Date<\/span><span class=\"br0\">&#40;<\/span>s<span class=\"br0\">&#41;<\/span><span class=\"sy0\">,<\/span><br \/>\n&nbsp; <span class=\"br0\">&#125;<\/span><span class=\"br0\">&#41;<\/span><span class=\"sy0\">,<\/span><br \/>\n&nbsp; <span class=\"br0\">&#91;<\/span><span class=\"br0\">&#93;<\/span><span class=\"sy0\">,<\/span><br \/>\n<span class=\"br0\">&#41;<\/span><span class=\"sy0\">;<\/span><\/div><\/div>\n<p>The <strong>parse_date<\/strong> defines how Gantt reads incoming dates, the <strong>format_date<\/strong> &#8211; how it writes them back. Gantt detects ISO strings automatically, so both overrides are optional. The starter keeps them to make the conversion visible. Delete them in your own project unless you are on an earlier version or your API does not use ISO 8601.<\/p>\n<h3>Transferring Gantt UI Changes to the Server<\/h3>\n<p>Every user interaction (dragging a bar, drawing a dependency, deleting a task) is tracked via the <strong>data.save<\/strong> callback with the following signature:<\/p>\n<div class=\"codecolorer-container javascript default\" style=\"overflow:auto;white-space:nowrap;\"><div class=\"javascript codecolorer\"><span class=\"br0\">&#40;<\/span>entity<span class=\"sy0\">,<\/span> action<span class=\"sy0\">,<\/span> payload<span class=\"sy0\">,<\/span> id<span class=\"br0\">&#41;<\/span> <span class=\"sy0\">=&gt;<\/span> <span class=\"kw1\">void<\/span><\/div><\/div>\n<p>Here, the entity is &#8220;task&#8221; or &#8220;link&#8221;, and the action is &#8220;create&#8221;, &#8220;update&#8221;, or &#8220;delete&#8221;. In the Zustand-only setup from the previous tutorial, this callback wrote to the store directly. Now it dispatches updates to one of six mutations, one per entity and action combination:<\/p>\n<div class=\"codecolorer-container javascript default\" style=\"overflow:auto;white-space:nowrap;\"><div class=\"javascript codecolorer\"><span class=\"kw1\">const<\/span> data<span class=\"sy0\">:<\/span> ReactGanttProps<span class=\"br0\">&#91;<\/span><span class=\"st0\">'data'<\/span><span class=\"br0\">&#93;<\/span> <span class=\"sy0\">=<\/span> useMemo<span class=\"br0\">&#40;<\/span><br \/>\n&nbsp; <span class=\"br0\">&#40;<\/span><span class=\"br0\">&#41;<\/span> <span class=\"sy0\">=&gt;<\/span> <span class=\"br0\">&#40;<\/span><span class=\"br0\">&#123;<\/span><br \/>\n&nbsp; &nbsp; save<span class=\"sy0\">:<\/span> <span class=\"br0\">&#40;<\/span>entity<span class=\"sy0\">,<\/span> action<span class=\"sy0\">,<\/span> payload<span class=\"sy0\">,<\/span> id<span class=\"br0\">&#41;<\/span> <span class=\"sy0\">=&gt;<\/span> <span class=\"br0\">&#123;<\/span><br \/>\n&nbsp; &nbsp; &nbsp; <span class=\"kw1\">if<\/span> <span class=\"br0\">&#40;<\/span>entity <span class=\"sy0\">===<\/span> <span class=\"st0\">'task'<\/span><span class=\"br0\">&#41;<\/span> <span class=\"br0\">&#123;<\/span><br \/>\n&nbsp; &nbsp; &nbsp; &nbsp; <span class=\"kw1\">const<\/span> task <span class=\"sy0\">=<\/span> payload as SerializedTask<span class=\"sy0\">;<\/span><br \/>\n&nbsp; &nbsp; &nbsp; &nbsp; <span class=\"kw1\">if<\/span> <span class=\"br0\">&#40;<\/span>action <span class=\"sy0\">===<\/span> <span class=\"st0\">'create'<\/span><span class=\"br0\">&#41;<\/span> <span class=\"kw1\">return<\/span> createTaskMutation.<span class=\"me1\">mutate<\/span><span class=\"br0\">&#40;<\/span>task<span class=\"br0\">&#41;<\/span><span class=\"sy0\">;<\/span><br \/>\n&nbsp; &nbsp; &nbsp; &nbsp; <span class=\"kw1\">else<\/span> <span class=\"kw1\">if<\/span> <span class=\"br0\">&#40;<\/span>action <span class=\"sy0\">===<\/span> <span class=\"st0\">'update'<\/span><span class=\"br0\">&#41;<\/span> updateTaskMutation.<span class=\"me1\">mutate<\/span><span class=\"br0\">&#40;<\/span>task<span class=\"br0\">&#41;<\/span><span class=\"sy0\">;<\/span><br \/>\n&nbsp; &nbsp; &nbsp; &nbsp; <span class=\"kw1\">else<\/span> <span class=\"kw1\">if<\/span> <span class=\"br0\">&#40;<\/span>action <span class=\"sy0\">===<\/span> <span class=\"st0\">'delete'<\/span><span class=\"br0\">&#41;<\/span> deleteTaskMutation.<span class=\"me1\">mutate<\/span><span class=\"br0\">&#40;<\/span>id<span class=\"br0\">&#41;<\/span><span class=\"sy0\">;<\/span><br \/>\n&nbsp; &nbsp; &nbsp; <span class=\"br0\">&#125;<\/span> <span class=\"kw1\">else<\/span> <span class=\"kw1\">if<\/span> <span class=\"br0\">&#40;<\/span>entity <span class=\"sy0\">===<\/span> <span class=\"st0\">'link'<\/span><span class=\"br0\">&#41;<\/span> <span class=\"br0\">&#123;<\/span><br \/>\n&nbsp; &nbsp; &nbsp; &nbsp; <span class=\"kw1\">const<\/span> link <span class=\"sy0\">=<\/span> payload as Link<span class=\"sy0\">;<\/span><br \/>\n&nbsp; &nbsp; &nbsp; &nbsp; <span class=\"kw1\">if<\/span> <span class=\"br0\">&#40;<\/span>action <span class=\"sy0\">===<\/span> <span class=\"st0\">'create'<\/span><span class=\"br0\">&#41;<\/span> <span class=\"kw1\">return<\/span> createLinkMutation.<span class=\"me1\">mutate<\/span><span class=\"br0\">&#40;<\/span>link<span class=\"br0\">&#41;<\/span><span class=\"sy0\">;<\/span><br \/>\n&nbsp; &nbsp; &nbsp; &nbsp; <span class=\"kw1\">else<\/span> <span class=\"kw1\">if<\/span> <span class=\"br0\">&#40;<\/span>action <span class=\"sy0\">===<\/span> <span class=\"st0\">'update'<\/span><span class=\"br0\">&#41;<\/span> updateLinkMutation.<span class=\"me1\">mutate<\/span><span class=\"br0\">&#40;<\/span>link<span class=\"br0\">&#41;<\/span><span class=\"sy0\">;<\/span><br \/>\n&nbsp; &nbsp; &nbsp; &nbsp; <span class=\"kw1\">else<\/span> <span class=\"kw1\">if<\/span> <span class=\"br0\">&#40;<\/span>action <span class=\"sy0\">===<\/span> <span class=\"st0\">'delete'<\/span><span class=\"br0\">&#41;<\/span> deleteLinkMutation.<span class=\"me1\">mutate<\/span><span class=\"br0\">&#40;<\/span>id<span class=\"br0\">&#41;<\/span><span class=\"sy0\">;<\/span><br \/>\n&nbsp; &nbsp; &nbsp; <span class=\"br0\">&#125;<\/span><br \/>\n&nbsp; &nbsp; <span class=\"br0\">&#125;<\/span><span class=\"sy0\">,<\/span><br \/>\n<span class=\"br0\">&#125;<\/span><span class=\"br0\">&#41;<\/span><span class=\"sy0\">,<\/span> <span class=\"br0\">&#91;<\/span><span class=\"coMULTI\">\/* mutation references *\/<\/span><span class=\"br0\">&#93;<\/span><span class=\"br0\">&#41;<\/span><span class=\"sy0\">;<\/span><\/div><\/div>\n<p>All six mutations share the same shape:<\/p>\n<div class=\"codecolorer-container javascript default\" style=\"overflow:auto;white-space:nowrap;\"><div class=\"javascript codecolorer\"><span class=\"kw1\">const<\/span> createTaskMutation <span class=\"sy0\">=<\/span> useMutation<span class=\"br0\">&#40;<\/span><span class=\"br0\">&#123;<\/span><br \/>\n&nbsp; mutationFn<span class=\"sy0\">:<\/span> createTask<span class=\"sy0\">,<\/span><br \/>\n&nbsp; onMutate<span class=\"sy0\">:<\/span> <span class=\"br0\">&#40;<\/span><span class=\"br0\">&#41;<\/span> <span class=\"sy0\">=&gt;<\/span> <span class=\"br0\">&#123;<\/span><br \/>\n&nbsp; &nbsp; recordHistory<span class=\"br0\">&#40;<\/span>makeSnapshot<span class=\"br0\">&#40;<\/span><span class=\"br0\">&#41;<\/span><span class=\"br0\">&#41;<\/span><span class=\"sy0\">;<\/span><br \/>\n&nbsp; <span class=\"br0\">&#125;<\/span><span class=\"sy0\">,<\/span><br \/>\n&nbsp; onSuccess<span class=\"sy0\">:<\/span> <span class=\"br0\">&#40;<\/span><span class=\"br0\">&#41;<\/span> <span class=\"sy0\">=&gt;<\/span> queryClient.<span class=\"me1\">invalidateQueries<\/span><span class=\"br0\">&#40;<\/span><span class=\"br0\">&#123;<\/span> queryKey<span class=\"sy0\">:<\/span> <span class=\"br0\">&#91;<\/span><span class=\"st0\">'data'<\/span><span class=\"br0\">&#93;<\/span> <span class=\"br0\">&#125;<\/span><span class=\"br0\">&#41;<\/span><span class=\"sy0\">,<\/span><br \/>\n&nbsp; onError<span class=\"sy0\">,<\/span><br \/>\n<span class=\"br0\">&#125;<\/span><span class=\"br0\">&#41;<\/span><span class=\"sy0\">;<\/span><\/div><\/div>\n<p>The <strong>useMutation<\/strong> hook allows you to run extra logic at different stages of a mutation through its lifecycle callbacks. The starter uses three of them. The <strong>onMutate<\/strong> callback fires first, pushing a copy of the current data onto the undo stack. After a successful request, the <strong>onSuccess<\/strong> callback refreshes cached data. If the request fails, the <strong>onError<\/strong> callback handles an error.<\/p>\n<p>For a detailed explanation of how <strong>data.save<\/strong> works, refer to the <a href=\"https:\/\/docs.dhtmlx.com\/gantt\/integrations\/react\/state\/state-management-basics\/#handlingchangeswithdatasave\" rel=\"noopener\" target=\"_blank\">Data Binding &#038; State Management basics<\/a> guide.<\/p>\n<h3>Cache Invalidation vs Optimistic Updates in React Gantt<\/h3>\n<p>Optimistic updates are among the headline features of TanStack Query, and this is the one the starter leaves untouched. The demo backend shows why:<\/p>\n<div class=\"codecolorer-container javascript default\" style=\"overflow:auto;white-space:nowrap;\"><div class=\"javascript codecolorer\">app.<span class=\"me1\">post<\/span><span class=\"br0\">&#40;<\/span><span class=\"st0\">'\/tasks'<\/span><span class=\"sy0\">,<\/span> <span class=\"br0\">&#40;<\/span>req<span class=\"sy0\">,<\/span> res<span class=\"br0\">&#41;<\/span> <span class=\"sy0\">=&gt;<\/span> <span class=\"br0\">&#123;<\/span><br \/>\n&nbsp; <span class=\"kw1\">const<\/span> db <span class=\"sy0\">=<\/span> readDB<span class=\"br0\">&#40;<\/span><span class=\"br0\">&#41;<\/span><span class=\"sy0\">;<\/span><br \/>\n&nbsp; <span class=\"kw1\">const<\/span> task <span class=\"sy0\">=<\/span> req.<span class=\"me1\">body<\/span> as Task<span class=\"sy0\">;<\/span><br \/>\n&nbsp; <span class=\"kw1\">const<\/span> newTask <span class=\"sy0\">=<\/span> <span class=\"br0\">&#123;<\/span> ...<span class=\"me1\">task<\/span><span class=\"sy0\">,<\/span> id<span class=\"sy0\">:<\/span> `DB_ID<span class=\"sy0\">:<\/span>$<span class=\"br0\">&#123;<\/span>task.<span class=\"me1\">id<\/span><span class=\"br0\">&#125;<\/span>` <span class=\"br0\">&#125;<\/span><span class=\"sy0\">;<\/span><br \/>\n&nbsp; db.<span class=\"me1\">tasks<\/span>.<span class=\"me1\">push<\/span><span class=\"br0\">&#40;<\/span>newTask<span class=\"br0\">&#41;<\/span><span class=\"sy0\">;<\/span><br \/>\n&nbsp; writeDB<span class=\"br0\">&#40;<\/span>db<span class=\"br0\">&#41;<\/span><span class=\"sy0\">;<\/span><br \/>\n&nbsp; res.<span class=\"me1\">json<\/span><span class=\"br0\">&#40;<\/span>newTask<span class=\"br0\">&#41;<\/span><span class=\"sy0\">;<\/span><br \/>\n<span class=\"br0\">&#125;<\/span><span class=\"br0\">&#41;<\/span><span class=\"sy0\">;<\/span><\/div><\/div>\n<p>The server does not accept client IDs. The backend generates a new ID with a <strong>DB_ID:<\/strong> prefix, the way most backends with auto-increment or UUID keys do.<\/p>\n<p>When a user creates a task, the Gantt generates a temporary client-side ID and draws the row. The real ID only exists after the server responds. Every link referencing that task has to point at the new value. Writing an optimistic row into the cache would mean writing a row with an ID you already know is wrong. So the starter refetches after the response and treats the server&#8217;s answer as authoritative.<\/p>\n<p>The notable exception is an API that accepts client-generated UUIDs. If yours does, the ID is known before the request, and optimistic creates become a real option.<\/p>\n<p>React Gantt has a first-class mechanism for this case, and the starter skips it. If <strong>save<\/strong> returns a promise that resolves to <strong>{ id: newId }<\/strong>, the chart swaps the temporary ID for the permanent one itself and repoints the links that referenced it. No refetch involved. With TanStack Query, it means reaching for <strong>mutateAsync<\/strong> instead of <strong>mutate<\/strong>:<\/p>\n<div class=\"codecolorer-container javascript default\" style=\"overflow:auto;white-space:nowrap;\"><div class=\"javascript codecolorer\"><span class=\"kw1\">if<\/span> <span class=\"br0\">&#40;<\/span>action <span class=\"sy0\">===<\/span> <span class=\"st0\">'create'<\/span><span class=\"br0\">&#41;<\/span> <span class=\"br0\">&#123;<\/span><br \/>\n&nbsp; <span class=\"kw1\">return<\/span> createTaskMutation.<span class=\"me1\">mutateAsync<\/span><span class=\"br0\">&#40;<\/span>payload as SerializedTask<span class=\"br0\">&#41;<\/span><br \/>\n&nbsp; &nbsp; .<span class=\"me1\">then<\/span><span class=\"br0\">&#40;<\/span><span class=\"br0\">&#40;<\/span>saved<span class=\"br0\">&#41;<\/span> <span class=\"sy0\">=&gt;<\/span> <span class=\"br0\">&#40;<\/span><span class=\"br0\">&#123;<\/span> id<span class=\"sy0\">:<\/span> saved.<span class=\"me1\">id<\/span> <span class=\"br0\">&#125;<\/span><span class=\"br0\">&#41;<\/span><span class=\"br0\">&#41;<\/span><span class=\"sy0\">;<\/span><br \/>\n<span class=\"br0\">&#125;<\/span><\/div><\/div>\n<p>The <strong>api.ts<\/strong> helpers already return the parsed response body from the create calls, so the ID is right there. The starter discards it. What you buy by ignoring it is uniformity: all six mutations end up with the same shape, and there is one reconciliation strategy to reason about instead of two. That trade works for a demo. On a chart with thousands of rows it stops working, because a full refetch per creation is the cost you were trying to avoid in the first place.<\/p>\n<p>Updates and deletes do not have this problem, since their IDs are already stable. You can apply them optimistically to make the app more responsive. A reasonable middle ground is to keep creation authoritative (via the returned ID or a refetch) and make updates optimistic, rather than committing to a single strategy for all six mutations.<\/p>\n<h3>Complementing React Gantt with Undo\/Redo Functionality<\/h3>\n<p>The undo operation involves both layers, because data and its history live in different places. Zustand holds stacks and stores plain snapshots:<\/p>\n<div class=\"codecolorer-container javascript default\" style=\"overflow:auto;white-space:nowrap;\"><div class=\"javascript codecolorer\"><span class=\"kw5\">export<\/span> type Snapshot <span class=\"sy0\">=<\/span> <span class=\"br0\">&#123;<\/span><br \/>\n&nbsp; tasks<span class=\"sy0\">:<\/span> SerializedTask<span class=\"br0\">&#91;<\/span><span class=\"br0\">&#93;<\/span><span class=\"sy0\">;<\/span><br \/>\n&nbsp; links<span class=\"sy0\">:<\/span> Link<span class=\"br0\">&#91;<\/span><span class=\"br0\">&#93;<\/span><span class=\"sy0\">;<\/span><br \/>\n&nbsp; config<span class=\"sy0\">:<\/span> GanttConfig<span class=\"sy0\">;<\/span><br \/>\n<span class=\"br0\">&#125;<\/span><span class=\"sy0\">;<\/span><\/div><\/div>\n<p>Keeping config next to the data means a zoom change is undone along with the edit. Users notice immediately when that is missing. The <strong>makeSnapshot<\/strong> helper clones all three with <strong>structuredClone<\/strong>, since the chart edits its task objects in place. The stacks are capped at 50 entries, and pushing a snapshot clears the redo branch, as users expect.<\/p>\n<p>Undo and redo actions in the store take the current snapshot and return the target one. They never import <strong>queryClient<\/strong> and have no idea where the data they are shuffling lives. The component is the only place the two layers meet:<\/p>\n<div class=\"codecolorer-container javascript default\" style=\"overflow:auto;white-space:nowrap;\"><div class=\"javascript codecolorer\"><span class=\"kw1\">const<\/span> handleUndo <span class=\"sy0\">=<\/span> <span class=\"br0\">&#40;<\/span><span class=\"br0\">&#41;<\/span> <span class=\"sy0\">=&gt;<\/span> <span class=\"br0\">&#123;<\/span><br \/>\n&nbsp; <span class=\"kw1\">const<\/span> snapshot <span class=\"sy0\">=<\/span> undo<span class=\"br0\">&#40;<\/span>makeSnapshot<span class=\"br0\">&#40;<\/span><span class=\"br0\">&#41;<\/span><span class=\"br0\">&#41;<\/span><span class=\"sy0\">;<\/span><br \/>\n&nbsp; <span class=\"kw1\">if<\/span> <span class=\"br0\">&#40;<\/span>snapshot<span class=\"br0\">&#41;<\/span> <span class=\"br0\">&#123;<\/span><br \/>\n&nbsp; &nbsp; queryClient.<span class=\"me1\">setQueryData<\/span><span class=\"br0\">&#40;<\/span><span class=\"br0\">&#91;<\/span><span class=\"st0\">'data'<\/span><span class=\"br0\">&#93;<\/span><span class=\"sy0\">,<\/span> snapshot<span class=\"br0\">&#41;<\/span><span class=\"sy0\">;<\/span><br \/>\n&nbsp; <span class=\"br0\">&#125;<\/span><br \/>\n<span class=\"br0\">&#125;<\/span><span class=\"sy0\">;<\/span><\/div><\/div>\n<p>Because <strong>setQueryData<\/strong> bypasses the network, the chart redraws instantly. Tasks and links come back through the cache. The zoom level travels a different route: undo updates config in the store, and the chart receives it through the config prop.<\/p>\n<p>One boundary is worth stating plainly: undo restores the cache and tells the server nothing. With the library defaults, data goes stale immediately and is refetched on window focus or remount, so the undone change comes back. Raising <strong>staleTime<\/strong> and disabling <strong>refetchOnWindowFocus<\/strong> narrows that window. If undo has to be durable, route it through a mutation like any other change. Send the restored snapshot to a batch endpoint and let onSuccess invalidate. Only <strong>handleUndo<\/strong> changes. The Zustand store stays as it is.<\/p>\n<p>For more information on integrating DHTMLX React Gantt with TanStack Query and Zustand, check out the <a href=\"https:\/\/docs.dhtmlx.com\/gantt\/integrations\/react\/state\/tanstack-query\/\" rel=\"noopener\" target=\"_blank\">official guide<\/a> available in our documentation.<\/p>\n<h2>Wrapping Up<\/h2>\n<p>The combination of TanStack Query is a practical solution for handling both server and local state in React Gantt apps.  With TanStack Query, you don&#8217;t have to worry about fetching, caching, and synchronizing Gantt data with the backend, while Zustand deals with the local UI state (undo\/redo, zooming). This clear separation of responsibilities gives a flexible React state management setup and simplifies the overall maintainability of the application architecture. Our starter demo and the official integration guide can help you apply this approach in real projects built around DHTMLX React Gantt.<\/p>\n<h2>Related Materials<\/h2>\n<ul>\n<li><a href=\"https:\/\/docs.dhtmlx.com\/gantt\/integrations\/react\/\" rel=\"noopener\" target=\"_blank\">Full React Gantt documentation<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/DHTMLX\/react-gantt-tanstack-query-starter\" rel=\"noopener\" target=\"_blank\">Runnable starter demo (React Gantt+Tanstack Query+Zustand)<\/a><\/li>\n<li><a href=\"https:\/\/docs.dhtmlx.com\/gantt\/integrations\/react\/state\/tanstack-query\/\" rel=\"noopener\" target=\"_blank\">Step-by-step integration guide<\/a><\/li>\n<li><a href=\"https:\/\/www.npmjs.com\/package\/@dhtmlx\/trial-react-gantt\" rel=\"noopener\" target=\"_blank\">Trial package on npm (DHTMLX React Gantt)<\/a><\/li>\n<li><a href=\"https:\/\/dhtmlx.com\/blog\/tutorials\/?tag=react\" rel=\"noopener\" target=\"_blank\">More tutorials for React<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>In one of our previous tutorials, we covered the basics of state management in DHTMLX React Gantt with Zustand. There, we focused on managing Gantt data on the client &#8230;<\/p>\n","protected":false},"author":101014,"featured_media":15875,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[71],"tags":[33,20,56],"class_list":["post-15871","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-tutorials","tag-all-posts","tag-gantt","tag-react"],"_links":{"self":[{"href":"https:\/\/dhtmlx.com\/blog\/wp-json\/wp\/v2\/posts\/15871","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dhtmlx.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/dhtmlx.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/dhtmlx.com\/blog\/wp-json\/wp\/v2\/users\/101014"}],"replies":[{"embeddable":true,"href":"https:\/\/dhtmlx.com\/blog\/wp-json\/wp\/v2\/comments?post=15871"}],"version-history":[{"count":6,"href":"https:\/\/dhtmlx.com\/blog\/wp-json\/wp\/v2\/posts\/15871\/revisions"}],"predecessor-version":[{"id":15873,"href":"https:\/\/dhtmlx.com\/blog\/wp-json\/wp\/v2\/posts\/15871\/revisions\/15873"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/dhtmlx.com\/blog\/wp-json\/wp\/v2\/media\/15875"}],"wp:attachment":[{"href":"https:\/\/dhtmlx.com\/blog\/wp-json\/wp\/v2\/media?parent=15871"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dhtmlx.com\/blog\/wp-json\/wp\/v2\/categories?post=15871"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dhtmlx.com\/blog\/wp-json\/wp\/v2\/tags?post=15871"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}