Skip to content

Commit ad4ff3c

Browse files
Merge pull request #96 from AlexKlimenkov/master
[dev] update tanstack-supabase.md guide
2 parents 9329a28 + 02132a7 commit ad4ff3c

8 files changed

Lines changed: 143 additions & 42 deletions

File tree

docs/integrations/angular/configuration-props.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ resetFilter(): void {
394394
</dhx-gantt>
395395
~~~
396396

397-
Keep a stable reference when the filter logic has not changed the wrapper compares by identity and re-renders only when the reference changes.
397+
Keep a stable reference when the filter logic has not changed - the wrapper compares by identity and re-renders only when the reference changes.
398398

399399
## Exported Types And Helpers
400400

@@ -418,8 +418,8 @@ Useful public exports from the wrapper package:
418418

419419
The wrapper exports two task-related types:
420420

421-
- **`SerializedTask`** use for data you own: store state, API responses, initial literals, `batchSave` payloads. Dates can be `Date` objects or strings matching `date_format`.
422-
- **`Task`** (re-exported from `@dhx/gantt`) for data gantt owns: inside event handlers, after gantt parses. Dates are `Date` objects. Has `$`-prefixed system properties.
421+
- **`SerializedTask`** - use for data you own: store state, API responses, initial literals, `batchSave` payloads. Dates can be `Date` objects or strings matching `date_format`.
422+
- **`Task`** (re-exported from `@dhx/gantt`) - for data gantt owns: inside event handlers, after gantt parses. Dates are `Date` objects. Has `$`-prefixed system properties.
423423

424424
`SerializedLink` is the link-side counterpart of `SerializedTask`.
425425

docs/integrations/angular/quick-start.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ If you later move the Gantt CSS import (or overrides for internal Gantt classes
6464

6565
Create `src/app/demo-data.ts`.
6666

67-
The wrapper exports `SerializedTask` and `SerializedLink` the recommended types for task/link data held outside gantt. Dates can be strings or `Date` objects.
67+
The wrapper exports `SerializedTask` and `SerializedLink` - the recommended types for task/link data held outside gantt. Dates can be strings or `Date` objects.
6868

6969
~~~ts
7070
import type { SerializedTask, SerializedLink } from '@dhtmlx/trial-angular-gantt';

docs/integrations/angular/state/rxjs.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ This is the core reducer-like step for grouped Gantt changes.
6868

6969
Create `src/app/gantt-state/gantt-rx-store.service.ts`.
7070

71-
The wrapper exports `SerializedTask` and `SerializedLink` use these to type tasks and links held outside of gantt (store state, API responses, initial data). Dates can be `Date` objects or strings.
71+
The wrapper exports `SerializedTask` and `SerializedLink` - use these to type tasks and links held outside of gantt (store state, API responses, initial data). Dates can be `Date` objects or strings.
7272

7373
~~~ts
7474
import { Injectable } from '@angular/core';

docs/integrations/react/configuration-props.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ The `@dhx/react-gantt` package re-exports several TypeScript types that you can
130130

131131
**When to use `SerializedTask` / `SerializedLink` vs `Task` / `Link`:**
132132

133-
- **`SerializedTask` / `SerializedLink`** for data you own: store state, API responses, initial data literals. Date fields accept strings (e.g. ISO dates).
134-
- **`Task` / `Link`** for data Gantt owns: inside event handlers, after Gantt parses the data. Date fields are `Date` objects. `Task` includes `$`-prefixed internal properties.
133+
- **`SerializedTask` / `SerializedLink`** - for data you own: store state, API responses, initial data literals. Date fields accept strings (e.g. ISO dates).
134+
- **`Task` / `Link`** - for data Gantt owns: inside event handlers, after Gantt parses the data. Date fields are `Date` objects. `Task` includes `$`-prefixed internal properties.
135135

136136
## Example Usage
137137

docs/integrations/react/state/state-management-basics.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ you need to be sure not to unintentionally overwrite Gantt's internal state.
5151

5252
In this pattern, you hold all core collections in state and pass them as props (`tasks`, `links`, `resources`, `resourceAssignments`). Whenever the user modifies tasks or links inside the Gantt (for example, by creating or deleting a task), the Gantt triggers a callback. In this callback, you update your React state with the new or removed data. Once the state is updated, React re-renders the **ReactGantt** component, which in turn reads the updated props from the latest state.
5353

54-
When typing your state, use `SerializedTask` for tasks and `SerializedLink` for links. These types represent the user-facing data shape date fields accept `Date | string`, and there are no internal `$`-prefixed properties. Use `Task` and `Link` only when working with data inside Gantt event handlers, where Gantt has already parsed the data.
54+
When typing your state, use `SerializedTask` for tasks and `SerializedLink` for links. These types represent the user-facing data shape - date fields accept `Date | string`, and there are no internal `$`-prefixed properties. Use `Task` and `Link` only when working with data inside Gantt event handlers, where Gantt has already parsed the data.
5555

5656
### Minimal example with React state
5757

docs/integrations/react/state/tanstack-query.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -918,12 +918,19 @@ A complete working project that follows this tutorial is [provided on GitHub](ht
918918

919919
## What's next
920920

921-
To go further:
921+
This is the second tutorial in the React Gantt state-management sequence:
922+
923+
1. [Zustand](integrations/react/state/zustand.md) - local in-memory state
924+
2. **TanStack Query** - server-backed state with a JSON file backend (you are here)
925+
3. [TanStack Query + Supabase](integrations/react/state/tanstack-supabase.md) - real-time multi-user sync over PostgreSQL
926+
927+
Ready to swap the JSON backend for a real database with live multi-user sync? Continue with [Using React Gantt with TanStack Query and Supabase](integrations/react/state/tanstack-supabase.md).
928+
929+
You can also:
922930

923931
- Revisit the concepts behind this example in [](integrations/react/state/state-management-basics.md)
924932
- Combine store-driven state with advanced configuration and templating in the [React Gantt overview](integrations/react/overview.md)
925933
- Explore the same pattern with other state managers:
926-
- [Using React Gantt with Zustand](integrations/react/state/zustand.md)
927934
- [Using React Gantt with Redux Toolkit](integrations/react/state/redux-toolkit.md)
928935
- [Using React Gantt with MobX](integrations/react/state/mobx.md)
929936
- [Using React Gantt with XState](integrations/react/state/xstate.md)

docs/integrations/react/state/tanstack-supabase.md

Lines changed: 117 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ Supabase Realtime must be enabled for both tables. In the Supabase dashboard go
141141

142142
The demo uses two separate Supabase client instances because the frontend and backend run in different environments.
143143

144-
`src/db/supabaseClient.ts` browser client, reads env via `import.meta.env`:
144+
`src/db/supabaseClient.ts` - browser client, reads env via `import.meta.env`:
145145

146146
```ts
147147
import { createClient, SupabaseClient } from '@supabase/supabase-js';
@@ -156,7 +156,7 @@ if (!supabaseUrlClient || !supabaseAnonKeyClient) {
156156
export const supabaseClient: SupabaseClient = createClient(supabaseUrlClient, supabaseAnonKeyClient);
157157
```
158158

159-
`src/db/supabaseServer.ts` server-side client, reads env via `process.env` (loaded by `dotenv`):
159+
`src/db/supabaseServer.ts` - server-side client, reads env via `process.env` (loaded by `dotenv`):
160160

161161
```ts
162162
import { createClient } from '@supabase/supabase-js';
@@ -174,6 +174,10 @@ export const supabaseServer = createClient(supabaseUrlServer, supabaseAnonKeySer
174174

175175
`supabaseClient` is used exclusively for Realtime subscriptions in `GanttComponent.tsx`. All database writes go through `supabaseServer` in the Express layer.
176176

177+
:::note
178+
This starter uses the **anon key** server-side because the demo schema has no Row-Level Security policies and there is no authentication. In production with auth and RLS, the server should hold the **service role key** (kept off the frontend) to bypass RLS for trusted operations.
179+
:::
180+
177181
## TypeScript types
178182

179183
`src/types/types.ts` defines the database row shapes and service interfaces:
@@ -278,7 +282,7 @@ export function sanitize<T extends object>(obj: T): T {
278282

279283
Every write service calls `sanitize()` before inserting or updating. Add field names to `TEXT_FIELDS` when the schema gains additional user-editable text columns.
280284

281-
### taskService sortorder management
285+
### taskService - sortorder management
282286

283287
`src/services/taskService.ts` is the most complex service because it manages the persistent task order.
284288

@@ -471,7 +475,72 @@ The `PUT /tasks/:id` handler destructures `target` out of the request body befor
471475

472476
## Creating the API layer
473477

474-
`src/api.ts` is identical to the base TanStack Query demo - plain `fetch` wrappers that throw on non-2xx responses. One small difference: `updateTask`, `deleteTask`, `updateLink`, and `deleteLink` now return the server response JSON (the updated/deleted row) instead of discarding it. The returned `id` is used by mutations to register pending operations for deduplication.
478+
`src/api.ts` is similar to the base TanStack Query demo - plain `fetch` wrappers that throw on non-2xx responses. The key difference: every mutation now returns the server response JSON (the updated/deleted row) instead of discarding it. The returned `id` is used by mutations to register pending operations for deduplication.
479+
480+
```ts
481+
import { type Link, type SerializedTask } from '@dhtmlx/trial-react-gantt';
482+
483+
const BASE = window.location.origin;
484+
485+
async function request(url: string, options?: RequestInit) {
486+
const res = await fetch(url, options);
487+
if (!res.ok) {
488+
throw new Error(`${options?.method ?? 'GET'} ${url} failed: ${res.status}`);
489+
}
490+
return res;
491+
}
492+
493+
export const fetchData = async () => {
494+
const res = await request(`${BASE}/data`);
495+
return await res.json();
496+
};
497+
498+
export const createTask = async (task: SerializedTask) => {
499+
const res = await request(`${BASE}/tasks`, {
500+
method: 'POST',
501+
body: JSON.stringify(task),
502+
headers: { 'Content-Type': 'application/json' },
503+
});
504+
return await res.json();
505+
};
506+
507+
export const updateTask = async (task: SerializedTask) => {
508+
const res = await request(`${BASE}/tasks/${task.id}`, {
509+
method: 'PUT',
510+
body: JSON.stringify(task),
511+
headers: { 'Content-Type': 'application/json' },
512+
});
513+
return await res.json();
514+
};
515+
516+
export const deleteTask = async (id: string | number) => {
517+
const res = await request(`${BASE}/tasks/${id}`, { method: 'DELETE' });
518+
return await res.json();
519+
};
520+
521+
// createLink, updateLink, deleteLink follow the same pattern against /links
522+
```
523+
524+
Frontend requests hit the same origin as the Vite dev server (`http://localhost:3000`); a proxy in `vite.config.ts` forwards `/data`, `/tasks`, and `/links` to the Express backend on port 3001:
525+
526+
```ts
527+
import { defineConfig } from 'vite';
528+
import react from '@vitejs/plugin-react';
529+
import tailwindcss from '@tailwindcss/vite';
530+
import path from 'path';
531+
532+
const API_URL = 'http://localhost:3001';
533+
534+
export default defineConfig({
535+
plugins: [react(), tailwindcss()],
536+
resolve: { alias: { '@': path.resolve(__dirname, './src') } },
537+
server: {
538+
port: 3000,
539+
open: true,
540+
proxy: { '/data': API_URL, '/tasks': API_URL, '/links': API_URL },
541+
},
542+
});
543+
```
475544

476545
## Zustand store changes
477546

@@ -638,7 +707,7 @@ const postgresChangesHandler = useCallback(
638707
Without this pattern, every local mutation would trigger two refetches: one from `onSuccess` and one from the Realtime echo. With it, local changes invalidate the cache exactly once, and only changes from other clients cause an additional refetch.
639708

640709
:::note
641-
Drag-and-drop reorders update `sortorder` on multiple rows server-side. Only the primary task is registered in `pendingOperationsRef`; the side-effect `sortorder` updates on other tasks produce untracked Realtime events that slip through to `invalidateQueries`. This is harmless `sortorder` is server-only state, and TanStack Query deduplicates rapid invalidations into a single background refetch.
710+
Drag-and-drop reorders update `sortorder` on multiple rows server-side. Only the primary task is registered in `pendingOperationsRef`; the side-effect `sortorder` updates on other tasks produce untracked Realtime events that slip through to `invalidateQueries`. This is harmless - `sortorder` is server-only state, and TanStack Query deduplicates rapid invalidations into a single background refetch.
642711
:::
643712

644713
### batchSave instead of save
@@ -683,52 +752,61 @@ const data: ReactGanttProps['data'] = useMemo(
683752
Key differences from `save`:
684753

685754
- A single undo entry covers the entire batch, not individual sub-operations.
686-
- The snapshot recorded is `prevSnapshotRef.current` the state captured just before `batchSave` fired so undo always reverts the complete interaction.
755+
- The snapshot recorded is `prevSnapshotRef.current` - the state captured just before `batchSave` fired - so undo always reverts the complete interaction.
687756
- The Gantt calls `batchSave` once per user gesture even if that gesture produces multiple database writes.
688757

689758
For more about `batchSave` see [Data Binding & State Management Basics](integrations/react/state/state-management-basics.md).
690759

691760
### Persistence-aware undo/redo
692761

693-
In the base TanStack Query tutorial, `handleUndo` and `handleRedo` write a snapshot into the client cache with `setQueryData` and that is it changes are not persisted until the user makes the next manual edit.
762+
In the base TanStack Query tutorial, `handleUndo` and `handleRedo` write a snapshot into the client cache with `setQueryData` and that is it - changes are not persisted until the user makes the next manual edit.
694763

695764
In this demo, undo/redo must also persist the rollback to Supabase so that other connected clients see it. This is done with `applySnapshotDiff`:
696765

697766
```tsx
698767
const applySnapshotDiff = useCallback(
699768
async (from: Snapshot, to: Snapshot) => {
700769
const diff = diffSnapshots(from, to);
770+
771+
const mutations: Promise<unknown>[] = [];
772+
const mutateAsync = <T,>(fn: (arg: T) => Promise<unknown>, arg: T) => {
773+
mutations.push(fn(arg));
774+
};
775+
701776
isUndoRedoRef.current = true;
702777

703778
// Links must be deleted before tasks (FK), tasks must be created before links (FK)
704-
diff.links.deleted.forEach((id) => mutations.push(deleteLinkMutation.mutateAsync(id)));
705-
diff.links.updated.forEach((link) => mutations.push(updateLinkMutation.mutateAsync(link)));
706-
await Promise.allSettled(/* batch 1 */);
779+
diff.links.deleted.forEach((id) => mutateAsync(deleteLinkMutation.mutateAsync, id));
780+
diff.links.updated.forEach((link) => mutateAsync(updateLinkMutation.mutateAsync, link));
781+
const batch1 = await Promise.allSettled(mutations.splice(0));
782+
783+
diff.tasks.deleted.forEach((id) => mutateAsync(deleteTaskMutation.mutateAsync, id));
784+
diff.tasks.created.forEach((task) => mutateAsync(createTaskMutation.mutateAsync, task));
785+
diff.tasks.updated.forEach((task) => mutateAsync(updateTaskMutation.mutateAsync, task));
786+
const batch2 = await Promise.allSettled(mutations.splice(0));
707787

708-
diff.tasks.deleted.forEach((id) => mutations.push(deleteTaskMutation.mutateAsync(id)));
709-
diff.tasks.created.forEach((task) => mutations.push(createTaskMutation.mutateAsync(task)));
710-
diff.tasks.updated.forEach((task) => mutations.push(updateTaskMutation.mutateAsync(task)));
711-
await Promise.allSettled(/* batch 2 */);
788+
diff.links.created.forEach((link) => mutateAsync(createLinkMutation.mutateAsync, link));
789+
const batch3 = await Promise.allSettled(mutations.splice(0));
712790

713-
diff.links.created.forEach((link) => mutations.push(createLinkMutation.mutateAsync(link)));
714-
await Promise.allSettled(/* batch 3 */);
791+
const results = [...batch1, ...batch2, ...batch3];
792+
const rejected = results.filter((result) => result.status === 'rejected');
715793

716794
isUndoRedoRef.current = false;
717795

718796
if (rejected.length) {
719-
console.error('Mutation failed:', rejected);
720-
queryClient.invalidateQueries({ queryKey: ['data'] });
721-
}
722-
},
723-
[
724-
createTaskMutation,
725-
updateTaskMutation,
726-
deleteTaskMutation,
727-
createLinkMutation,
728-
updateLinkMutation,
729-
deleteLinkMutation,
730-
queryClient,
731-
],
797+
console.error('Mutation failed:', rejected);
798+
queryClient.invalidateQueries({ queryKey: ['data'] });
799+
}
800+
},
801+
[
802+
createTaskMutation,
803+
updateTaskMutation,
804+
deleteTaskMutation,
805+
createLinkMutation,
806+
updateLinkMutation,
807+
deleteLinkMutation,
808+
queryClient,
809+
],
732810
);
733811

734812
const handleUndo = () => {
@@ -809,10 +887,18 @@ The key architectural pattern is the **pending-operations set**: local mutations
809887

810888
## GitHub demo repository
811889

812-
The complete working project is [available on GitHub](https://github.com/dhtmlx/dhx-react-gantt-tanstack-supabase-demo).
890+
The complete working project is [available on GitHub](https://github.com/dhtmlx/react-gantt-tanstack-supabase-starter).
813891

814892
## What's next
815893

894+
This is the third tutorial in the React Gantt state-management sequence:
895+
896+
1. [Zustand](integrations/react/state/zustand.md) - local in-memory state
897+
2. [TanStack Query](integrations/react/state/tanstack-query.md) - server-backed state with a JSON file backend
898+
3. **TanStack Query + Supabase** - real-time multi-user sync (you are here)
899+
900+
From here you can:
901+
816902
- Revisit the core data binding concepts in [Data Binding & State Management Basics](integrations/react/state/state-management-basics.md)
817-
- Compare with the simpler local-storage version in [Using React Gantt with TanStack Query](integrations/react/state/tanstack-query.md)
903+
- Compare with the simpler local-backend version in [Using React Gantt with TanStack Query](integrations/react/state/tanstack-query.md)
818904
- Explore Realtime sync with a different backend in [Firebase Integration](integrations/react/firebase-integration.md)

docs/integrations/react/state/zustand.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,15 @@ A complete working project that follows this tutorial is [provided on GitHub](ht
486486

487487
## What's next
488488

489-
To go further:
489+
This is the first tutorial in the React Gantt state-management sequence:
490+
491+
1. **Zustand** - local in-memory state (you are here)
492+
2. [TanStack Query](integrations/react/state/tanstack-query.md) - server-backed state with a JSON file backend
493+
3. [TanStack Query + Supabase](integrations/react/state/tanstack-supabase.md) - real-time multi-user sync over PostgreSQL
494+
495+
Ready for a server-backed version? Continue with [Using React Gantt with TanStack Query](integrations/react/state/tanstack-query.md).
496+
497+
You can also:
490498

491499
- Revisit the concepts behind this example in [](integrations/react/state/state-management-basics.md)
492500
- Combine store-driven state with advanced configuration and templating in the [React Gantt overview](integrations/react/overview.md)

0 commit comments

Comments
 (0)