@critical-path/react 0.13.10 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/hooks.ts CHANGED
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import type { CreateAttachmentBody, CreateCommentBody, CreateTaskBody, UpdateCommentBody, UpdateDeliverableBody, UpdateTaskBody } from '@critical-path/core/schemas';
2
4
  import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
3
5
  import type {
@@ -36,21 +38,28 @@ export function useWorkflows() {
36
38
  const [loading, setLoading] = useState(true);
37
39
  const [error, setError] = useState<Error | null>(null);
38
40
 
39
- const fetchWorkflows = useCallback(async () => {
41
+ const fetchWorkflows = useCallback(async (signal?: AbortSignal) => {
42
+ const api = signal ? client.with({ signal }) : client;
40
43
  try {
41
44
  setLoading(true);
42
- const data = await client.getWorkflows();
45
+ const data = await api.getWorkflows();
46
+ if (signal?.aborted) return;
43
47
  setWorkflows(data);
44
48
  setError(null);
45
49
  } catch (err) {
50
+ if (signal?.aborted) return;
46
51
  setError(err instanceof Error ? err : new Error(String(err)));
47
52
  } finally {
48
- setLoading(false);
53
+ if (!signal?.aborted) setLoading(false);
49
54
  }
50
55
  }, [client]);
51
56
 
52
57
  useEffect(() => {
53
- fetchWorkflows();
58
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
59
+ // response for old inputs can never overwrite newer data.
60
+ const controller = new AbortController();
61
+ fetchWorkflows(controller.signal);
62
+ return () => controller.abort();
54
63
  }, [fetchWorkflows]);
55
64
 
56
65
  const createWorkflow = async (input: Omit<Workflow, 'id' | 'createdAt' | 'updatedAt'>) => {
@@ -88,7 +97,7 @@ export function useWorkflows() {
88
97
  }
89
98
  };
90
99
 
91
- return { workflows, loading, error, refresh: fetchWorkflows, createWorkflow, updateWorkflow, deleteWorkflow };
100
+ return { workflows, loading, error, refresh: () => fetchWorkflows(), createWorkflow, updateWorkflow, deleteWorkflow };
92
101
  }
93
102
 
94
103
  export function useTaskTransitions(taskId?: string) {
@@ -97,28 +106,35 @@ export function useTaskTransitions(taskId?: string) {
97
106
  const [loading, setLoading] = useState(false);
98
107
  const [error, setError] = useState<Error | null>(null);
99
108
 
100
- const fetchTransitions = useCallback(async () => {
109
+ const fetchTransitions = useCallback(async (signal?: AbortSignal) => {
110
+ const api = signal ? client.with({ signal }) : client;
101
111
  if (!taskId) {
102
112
  setAllowedTransitions([]);
103
113
  return;
104
114
  }
105
115
  try {
106
116
  setLoading(true);
107
- const data = await client.getAllowedTaskTransitions(taskId);
117
+ const data = await api.getAllowedTaskTransitions(taskId);
118
+ if (signal?.aborted) return;
108
119
  setAllowedTransitions(data);
109
120
  setError(null);
110
121
  } catch (err) {
122
+ if (signal?.aborted) return;
111
123
  setError(err instanceof Error ? err : new Error(String(err)));
112
124
  } finally {
113
- setLoading(false);
125
+ if (!signal?.aborted) setLoading(false);
114
126
  }
115
127
  }, [client, taskId]);
116
128
 
117
129
  useEffect(() => {
118
- fetchTransitions();
130
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
131
+ // response for old inputs can never overwrite newer data.
132
+ const controller = new AbortController();
133
+ fetchTransitions(controller.signal);
134
+ return () => controller.abort();
119
135
  }, [fetchTransitions]);
120
136
 
121
- return { allowedTransitions, loading, error, refresh: fetchTransitions };
137
+ return { allowedTransitions, loading, error, refresh: () => fetchTransitions() };
122
138
  }
123
139
 
124
140
  export function useProjects() {
@@ -127,21 +143,28 @@ export function useProjects() {
127
143
  const [loading, setLoading] = useState(true);
128
144
  const [error, setError] = useState<Error | null>(null);
129
145
 
130
- const fetchProjects = useCallback(async () => {
146
+ const fetchProjects = useCallback(async (signal?: AbortSignal) => {
147
+ const api = signal ? client.with({ signal }) : client;
131
148
  try {
132
149
  setLoading(true);
133
- const data = await client.getProjects();
150
+ const data = await api.getProjects();
151
+ if (signal?.aborted) return;
134
152
  setProjects(data);
135
153
  setError(null);
136
154
  } catch (err) {
155
+ if (signal?.aborted) return;
137
156
  setError(err instanceof Error ? err : new Error(String(err)));
138
157
  } finally {
139
- setLoading(false);
158
+ if (!signal?.aborted) setLoading(false);
140
159
  }
141
160
  }, [client]);
142
161
 
143
162
  useEffect(() => {
144
- fetchProjects();
163
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
164
+ // response for old inputs can never overwrite newer data.
165
+ const controller = new AbortController();
166
+ fetchProjects(controller.signal);
167
+ return () => controller.abort();
145
168
  }, [fetchProjects]);
146
169
 
147
170
  const createProject = async (input: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>) => {
@@ -168,7 +191,7 @@ export function useProjects() {
168
191
  }
169
192
  };
170
193
 
171
- return { projects, loading, error, refresh: fetchProjects, createProject };
194
+ return { projects, loading, error, refresh: () => fetchProjects(), createProject };
172
195
  }
173
196
 
174
197
  export function useTasks(projectId?: string) {
@@ -177,16 +200,19 @@ export function useTasks(projectId?: string) {
177
200
  const [loading, setLoading] = useState(true);
178
201
  const [error, setError] = useState<Error | null>(null);
179
202
 
180
- const fetchTasks = useCallback(async () => {
203
+ const fetchTasks = useCallback(async (signal?: AbortSignal) => {
204
+ const api = signal ? client.with({ signal }) : client;
181
205
  try {
182
206
  setLoading(true);
183
- const data = await client.getTasks(projectId);
207
+ const data = await api.getTasks(projectId);
208
+ if (signal?.aborted) return;
184
209
  setTasks(data);
185
210
  setError(null);
186
211
  } catch (err) {
212
+ if (signal?.aborted) return;
187
213
  setError(err instanceof Error ? err : new Error(String(err)));
188
214
  } finally {
189
- setLoading(false);
215
+ if (!signal?.aborted) setLoading(false);
190
216
  }
191
217
  }, [client, projectId]);
192
218
 
@@ -194,7 +220,11 @@ export function useTasks(projectId?: string) {
194
220
  const tempToRealIdMapRef = useRef<Map<string, string>>(new Map());
195
221
 
196
222
  useEffect(() => {
197
- fetchTasks();
223
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
224
+ // response for old inputs can never overwrite newer data.
225
+ const controller = new AbortController();
226
+ fetchTasks(controller.signal);
227
+ return () => controller.abort();
198
228
  }, [fetchTasks]);
199
229
 
200
230
  const createTask = async (input: CreateTaskBody) => {
@@ -353,7 +383,7 @@ export function useTasks(projectId?: string) {
353
383
  }
354
384
  };
355
385
 
356
- return { tasks, loading, error, refresh: fetchTasks, createTask, updateTask, updateTaskStatus, deleteTask };
386
+ return { tasks, loading, error, refresh: () => fetchTasks(), createTask, updateTask, updateTaskStatus, deleteTask };
357
387
  }
358
388
 
359
389
  export interface UseKanbanOptions {
@@ -424,7 +454,8 @@ export function useComments(taskId: string) {
424
454
  const [loading, setLoading] = useState(true);
425
455
  const [error, setError] = useState<Error | null>(null);
426
456
 
427
- const fetchComments = useCallback(async () => {
457
+ const fetchComments = useCallback(async (signal?: AbortSignal) => {
458
+ const api = signal ? client.with({ signal }) : client;
428
459
  if (!taskId) {
429
460
  setComments([]);
430
461
  setLoading(false);
@@ -432,18 +463,24 @@ export function useComments(taskId: string) {
432
463
  }
433
464
  try {
434
465
  setLoading(true);
435
- const data = await client.getComments(taskId);
466
+ const data = await api.getComments(taskId);
467
+ if (signal?.aborted) return;
436
468
  setComments(data);
437
469
  setError(null);
438
470
  } catch (err) {
471
+ if (signal?.aborted) return;
439
472
  setError(err instanceof Error ? err : new Error(String(err)));
440
473
  } finally {
441
- setLoading(false);
474
+ if (!signal?.aborted) setLoading(false);
442
475
  }
443
476
  }, [client, taskId]);
444
477
 
445
478
  useEffect(() => {
446
- fetchComments();
479
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
480
+ // response for old inputs can never overwrite newer data.
481
+ const controller = new AbortController();
482
+ fetchComments(controller.signal);
483
+ return () => controller.abort();
447
484
  }, [fetchComments]);
448
485
 
449
486
  const addComment = async (input: Omit<CreateCommentBody, 'taskId'>) => {
@@ -541,7 +578,7 @@ export function useComments(taskId: string) {
541
578
  threads,
542
579
  loading,
543
580
  error,
544
- refresh: fetchComments,
581
+ refresh: () => fetchComments(),
545
582
  addComment,
546
583
  updateComment,
547
584
  deleteComment,
@@ -558,21 +595,28 @@ export function useAttachments(filter?: { taskId?: string; projectId?: string; c
558
595
 
559
596
  const filterKey = JSON.stringify(filter || {});
560
597
 
561
- const fetchAttachments = useCallback(async () => {
598
+ const fetchAttachments = useCallback(async (signal?: AbortSignal) => {
599
+ const api = signal ? client.with({ signal }) : client;
562
600
  try {
563
601
  setLoading(true);
564
- const data = await client.getAttachments(filter);
602
+ const data = await api.getAttachments(filter);
603
+ if (signal?.aborted) return;
565
604
  setAttachments(data);
566
605
  setError(null);
567
606
  } catch (err) {
607
+ if (signal?.aborted) return;
568
608
  setError(err instanceof Error ? err : new Error(String(err)));
569
609
  } finally {
570
- setLoading(false);
610
+ if (!signal?.aborted) setLoading(false);
571
611
  }
572
612
  }, [client, filterKey]);
573
613
 
574
614
  useEffect(() => {
575
- fetchAttachments();
615
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
616
+ // response for old inputs can never overwrite newer data.
617
+ const controller = new AbortController();
618
+ fetchAttachments(controller.signal);
619
+ return () => controller.abort();
576
620
  }, [fetchAttachments]);
577
621
 
578
622
  const createAttachment = async (input: Omit<Attachment, 'id' | 'createdAt' | 'updatedAt'>) => {
@@ -602,7 +646,7 @@ export function useAttachments(filter?: { taskId?: string; projectId?: string; c
602
646
  attachments,
603
647
  loading,
604
648
  error,
605
- refresh: fetchAttachments,
649
+ refresh: () => fetchAttachments(),
606
650
  createAttachment,
607
651
  deleteAttachment
608
652
  };
@@ -614,7 +658,8 @@ export function useDeliverables(projectId?: string) {
614
658
  const [loading, setLoading] = useState(true);
615
659
  const [error, setError] = useState<Error | null>(null);
616
660
 
617
- const fetchDeliverables = useCallback(async () => {
661
+ const fetchDeliverables = useCallback(async (signal?: AbortSignal) => {
662
+ const api = signal ? client.with({ signal }) : client;
618
663
  if (!projectId) {
619
664
  setDeliverables([]);
620
665
  setLoading(false);
@@ -622,18 +667,24 @@ export function useDeliverables(projectId?: string) {
622
667
  }
623
668
  try {
624
669
  setLoading(true);
625
- const data = await client.getDeliverables(projectId);
670
+ const data = await api.getDeliverables(projectId);
671
+ if (signal?.aborted) return;
626
672
  setDeliverables(data);
627
673
  setError(null);
628
674
  } catch (err) {
675
+ if (signal?.aborted) return;
629
676
  setError(err instanceof Error ? err : new Error(String(err)));
630
677
  } finally {
631
- setLoading(false);
678
+ if (!signal?.aborted) setLoading(false);
632
679
  }
633
680
  }, [client, projectId]);
634
681
 
635
682
  useEffect(() => {
636
- fetchDeliverables();
683
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
684
+ // response for old inputs can never overwrite newer data.
685
+ const controller = new AbortController();
686
+ fetchDeliverables(controller.signal);
687
+ return () => controller.abort();
637
688
  }, [fetchDeliverables]);
638
689
 
639
690
  const createDeliverable = async (input: CreateDeliverableInput) => {
@@ -675,7 +726,7 @@ export function useDeliverables(projectId?: string) {
675
726
  deliverables,
676
727
  loading,
677
728
  error,
678
- refresh: fetchDeliverables,
729
+ refresh: () => fetchDeliverables(),
679
730
  createDeliverable,
680
731
  updateDeliverable,
681
732
  deleteDeliverable
@@ -688,7 +739,8 @@ export function useDeliverableSummary(deliverableId?: string) {
688
739
  const [loading, setLoading] = useState(true);
689
740
  const [error, setError] = useState<Error | null>(null);
690
741
 
691
- const fetchSummary = useCallback(async () => {
742
+ const fetchSummary = useCallback(async (signal?: AbortSignal) => {
743
+ const api = signal ? client.with({ signal }) : client;
692
744
  if (!deliverableId) {
693
745
  setSummary(null);
694
746
  setLoading(false);
@@ -696,25 +748,31 @@ export function useDeliverableSummary(deliverableId?: string) {
696
748
  }
697
749
  try {
698
750
  setLoading(true);
699
- const data = await client.getDeliverableSummary(deliverableId);
751
+ const data = await api.getDeliverableSummary(deliverableId);
752
+ if (signal?.aborted) return;
700
753
  setSummary(data);
701
754
  setError(null);
702
755
  } catch (err) {
756
+ if (signal?.aborted) return;
703
757
  setError(err instanceof Error ? err : new Error(String(err)));
704
758
  } finally {
705
- setLoading(false);
759
+ if (!signal?.aborted) setLoading(false);
706
760
  }
707
761
  }, [client, deliverableId]);
708
762
 
709
763
  useEffect(() => {
710
- fetchSummary();
764
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
765
+ // response for old inputs can never overwrite newer data.
766
+ const controller = new AbortController();
767
+ fetchSummary(controller.signal);
768
+ return () => controller.abort();
711
769
  }, [fetchSummary]);
712
770
 
713
771
  return {
714
772
  summary,
715
773
  loading,
716
774
  error,
717
- refresh: fetchSummary
775
+ refresh: () => fetchSummary()
718
776
  };
719
777
  }
720
778
 
@@ -781,7 +839,8 @@ export function useTaskActivity(taskId?: string) {
781
839
  const [loading, setLoading] = useState(false);
782
840
  const [error, setError] = useState<Error | null>(null);
783
841
 
784
- const fetchActivity = useCallback(async () => {
842
+ const fetchActivity = useCallback(async (signal?: AbortSignal) => {
843
+ const api = signal ? client.with({ signal }) : client;
785
844
  if (!taskId) {
786
845
  setComments([]);
787
846
  setAttachments([]);
@@ -791,21 +850,27 @@ export function useTaskActivity(taskId?: string) {
791
850
  try {
792
851
  setLoading(true);
793
852
  const [fetchedComments, fetchedAttachments] = await Promise.all([
794
- client.getComments(taskId),
795
- client.getAttachments({ taskId })
853
+ api.getComments(taskId),
854
+ api.getAttachments({ taskId })
796
855
  ]);
856
+ if (signal?.aborted) return;
797
857
  setComments(fetchedComments);
798
858
  setAttachments(fetchedAttachments);
799
859
  setError(null);
800
860
  } catch (err) {
861
+ if (signal?.aborted) return;
801
862
  setError(err instanceof Error ? err : new Error(String(err)));
802
863
  } finally {
803
- setLoading(false);
864
+ if (!signal?.aborted) setLoading(false);
804
865
  }
805
866
  }, [client, taskId]);
806
867
 
807
868
  useEffect(() => {
808
- fetchActivity();
869
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
870
+ // response for old inputs can never overwrite newer data.
871
+ const controller = new AbortController();
872
+ fetchActivity(controller.signal);
873
+ return () => controller.abort();
809
874
  }, [fetchActivity]);
810
875
 
811
876
  const threads = useMemo<ThreadedCommentWithAttachments[]>(() => {
@@ -969,7 +1034,7 @@ export function useTaskActivity(taskId?: string) {
969
1034
  standaloneAttachments,
970
1035
  loading,
971
1036
  error,
972
- refresh: fetchActivity,
1037
+ refresh: () => fetchActivity(),
973
1038
  addComment,
974
1039
  addAttachment,
975
1040
  deleteComment,
@@ -985,7 +1050,8 @@ export function useCriticalPath(projectId: string | undefined) {
985
1050
  const [loading, setLoading] = useState(true);
986
1051
  const [error, setError] = useState<Error | null>(null);
987
1052
 
988
- const fetchCriticalPath = useCallback(async () => {
1053
+ const fetchCriticalPath = useCallback(async (signal?: AbortSignal) => {
1054
+ const api = signal ? client.with({ signal }) : client;
989
1055
  if (!projectId) {
990
1056
  setAnalysis(null);
991
1057
  setLoading(false);
@@ -993,25 +1059,31 @@ export function useCriticalPath(projectId: string | undefined) {
993
1059
  }
994
1060
  try {
995
1061
  setLoading(true);
996
- const data = await client.calculateCriticalPath(projectId);
1062
+ const data = await api.calculateCriticalPath(projectId);
1063
+ if (signal?.aborted) return;
997
1064
  setAnalysis(data);
998
1065
  setError(null);
999
1066
  } catch (err) {
1067
+ if (signal?.aborted) return;
1000
1068
  setError(err instanceof Error ? err : new Error(String(err)));
1001
1069
  } finally {
1002
- setLoading(false);
1070
+ if (!signal?.aborted) setLoading(false);
1003
1071
  }
1004
1072
  }, [client, projectId]);
1005
1073
 
1006
1074
  useEffect(() => {
1007
- fetchCriticalPath();
1075
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
1076
+ // response for old inputs can never overwrite newer data.
1077
+ const controller = new AbortController();
1078
+ fetchCriticalPath(controller.signal);
1079
+ return () => controller.abort();
1008
1080
  }, [fetchCriticalPath]);
1009
1081
 
1010
1082
  return {
1011
1083
  analysis,
1012
1084
  loading,
1013
1085
  error,
1014
- refresh: fetchCriticalPath
1086
+ refresh: () => fetchCriticalPath()
1015
1087
  };
1016
1088
  }
1017
1089
 
@@ -1025,7 +1097,8 @@ export function useTimelineLadder(
1025
1097
  const [loading, setLoading] = useState(true);
1026
1098
  const [error, setError] = useState<Error | null>(null);
1027
1099
 
1028
- const fetchLadder = useCallback(async () => {
1100
+ const fetchLadder = useCallback(async (signal?: AbortSignal) => {
1101
+ const api = signal ? client.with({ signal }) : client;
1029
1102
  if (!projectId) {
1030
1103
  setLadder(null);
1031
1104
  setLoading(false);
@@ -1033,21 +1106,27 @@ export function useTimelineLadder(
1033
1106
  }
1034
1107
  try {
1035
1108
  setLoading(true);
1036
- const data = await client.getTimelineLadder(projectId, {
1109
+ const data = await api.getTimelineLadder(projectId, {
1037
1110
  ...initialOptions,
1038
1111
  level
1039
1112
  });
1113
+ if (signal?.aborted) return;
1040
1114
  setLadder(data);
1041
1115
  setError(null);
1042
1116
  } catch (err) {
1117
+ if (signal?.aborted) return;
1043
1118
  setError(err instanceof Error ? err : new Error(String(err)));
1044
1119
  } finally {
1045
- setLoading(false);
1120
+ if (!signal?.aborted) setLoading(false);
1046
1121
  }
1047
1122
  }, [client, projectId, level, initialOptions.containerId, initialOptions.iterationId]);
1048
1123
 
1049
1124
  useEffect(() => {
1050
- fetchLadder();
1125
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
1126
+ // response for old inputs can never overwrite newer data.
1127
+ const controller = new AbortController();
1128
+ fetchLadder(controller.signal);
1129
+ return () => controller.abort();
1051
1130
  }, [fetchLadder]);
1052
1131
 
1053
1132
  return {
@@ -1059,7 +1138,7 @@ export function useTimelineLadder(
1059
1138
  concrete: ladder?.concrete ?? null,
1060
1139
  loading,
1061
1140
  error,
1062
- refresh: fetchLadder
1141
+ refresh: () => fetchLadder()
1063
1142
  };
1064
1143
  }
1065
1144
 
@@ -1069,7 +1148,8 @@ export function useTaskLadder(taskId: string | undefined) {
1069
1148
  const [loading, setLoading] = useState(true);
1070
1149
  const [error, setError] = useState<Error | null>(null);
1071
1150
 
1072
- const fetchTaskLadder = useCallback(async () => {
1151
+ const fetchTaskLadder = useCallback(async (signal?: AbortSignal) => {
1152
+ const api = signal ? client.with({ signal }) : client;
1073
1153
  if (!taskId) {
1074
1154
  setTaskLadder(null);
1075
1155
  setLoading(false);
@@ -1077,18 +1157,24 @@ export function useTaskLadder(taskId: string | undefined) {
1077
1157
  }
1078
1158
  try {
1079
1159
  setLoading(true);
1080
- const data = await client.getTaskLadder(taskId);
1160
+ const data = await api.getTaskLadder(taskId);
1161
+ if (signal?.aborted) return;
1081
1162
  setTaskLadder(data);
1082
1163
  setError(null);
1083
1164
  } catch (err) {
1165
+ if (signal?.aborted) return;
1084
1166
  setError(err instanceof Error ? err : new Error(String(err)));
1085
1167
  } finally {
1086
- setLoading(false);
1168
+ if (!signal?.aborted) setLoading(false);
1087
1169
  }
1088
1170
  }, [client, taskId]);
1089
1171
 
1090
1172
  useEffect(() => {
1091
- fetchTaskLadder();
1173
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
1174
+ // response for old inputs can never overwrite newer data.
1175
+ const controller = new AbortController();
1176
+ fetchTaskLadder(controller.signal);
1177
+ return () => controller.abort();
1092
1178
  }, [fetchTaskLadder]);
1093
1179
 
1094
1180
  return {
@@ -1099,7 +1185,7 @@ export function useTaskLadder(taskId: string | undefined) {
1099
1185
  metrics: taskLadder?.metrics ?? null,
1100
1186
  loading,
1101
1187
  error,
1102
- refresh: fetchTaskLadder
1188
+ refresh: () => fetchTaskLadder()
1103
1189
  };
1104
1190
  }
1105
1191
 
@@ -1110,7 +1196,8 @@ export function useTaskMetrics(taskId: string | undefined) {
1110
1196
  const [loading, setLoading] = useState(true);
1111
1197
  const [error, setError] = useState<Error | null>(null);
1112
1198
 
1113
- const fetchMetrics = useCallback(async () => {
1199
+ const fetchMetrics = useCallback(async (signal?: AbortSignal) => {
1200
+ const api = signal ? client.with({ signal }) : client;
1114
1201
  if (!taskId) {
1115
1202
  setMetrics(null);
1116
1203
  setHistory(null);
@@ -1120,21 +1207,27 @@ export function useTaskMetrics(taskId: string | undefined) {
1120
1207
  try {
1121
1208
  setLoading(true);
1122
1209
  const [m, h] = await Promise.all([
1123
- client.getTaskMetrics(taskId),
1124
- client.getTaskProgressHistory(taskId)
1210
+ api.getTaskMetrics(taskId),
1211
+ api.getTaskProgressHistory(taskId)
1125
1212
  ]);
1213
+ if (signal?.aborted) return;
1126
1214
  setMetrics(m);
1127
1215
  setHistory(h);
1128
1216
  setError(null);
1129
1217
  } catch (err) {
1218
+ if (signal?.aborted) return;
1130
1219
  setError(err instanceof Error ? err : new Error(String(err)));
1131
1220
  } finally {
1132
- setLoading(false);
1221
+ if (!signal?.aborted) setLoading(false);
1133
1222
  }
1134
1223
  }, [client, taskId]);
1135
1224
 
1136
1225
  useEffect(() => {
1137
- fetchMetrics();
1226
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
1227
+ // response for old inputs can never overwrite newer data.
1228
+ const controller = new AbortController();
1229
+ fetchMetrics(controller.signal);
1230
+ return () => controller.abort();
1138
1231
  }, [fetchMetrics]);
1139
1232
 
1140
1233
  return {
@@ -1147,7 +1240,7 @@ export function useTaskMetrics(taskId: string | undefined) {
1147
1240
  curveProfile: history?.curveProfile ?? null,
1148
1241
  loading,
1149
1242
  error,
1150
- refresh: fetchMetrics
1243
+ refresh: () => fetchMetrics()
1151
1244
  };
1152
1245
  }
1153
1246
 
@@ -1163,21 +1256,24 @@ export function useWorkloadDistribution(
1163
1256
  const [loading, setLoading] = useState(true);
1164
1257
  const [error, setError] = useState<Error | null>(null);
1165
1258
 
1166
- const fetchWorkload = useCallback(async () => {
1259
+ const fetchWorkload = useCallback(async (signal?: AbortSignal) => {
1260
+ const api = signal ? client.with({ signal }) : client;
1167
1261
  try {
1168
1262
  setLoading(true);
1169
- const data = await client.getWorkloadDistribution(projectId, {
1263
+ const data = await api.getWorkloadDistribution(projectId, {
1170
1264
  ...initialOptions,
1171
1265
  interval,
1172
1266
  groupBy,
1173
1267
  metric
1174
1268
  });
1269
+ if (signal?.aborted) return;
1175
1270
  setWorkload(data);
1176
1271
  setError(null);
1177
1272
  } catch (err) {
1273
+ if (signal?.aborted) return;
1178
1274
  setError(err instanceof Error ? err : new Error(String(err)));
1179
1275
  } finally {
1180
- setLoading(false);
1276
+ if (!signal?.aborted) setLoading(false);
1181
1277
  }
1182
1278
  }, [
1183
1279
  client,
@@ -1191,7 +1287,11 @@ export function useWorkloadDistribution(
1191
1287
  ]);
1192
1288
 
1193
1289
  useEffect(() => {
1194
- fetchWorkload();
1290
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
1291
+ // response for old inputs can never overwrite newer data.
1292
+ const controller = new AbortController();
1293
+ fetchWorkload(controller.signal);
1294
+ return () => controller.abort();
1195
1295
  }, [fetchWorkload]);
1196
1296
 
1197
1297
  return {
@@ -1210,7 +1310,7 @@ export function useWorkloadDistribution(
1210
1310
  averageUtilization: workload?.averageUtilization,
1211
1311
  loading,
1212
1312
  error,
1213
- refresh: fetchWorkload
1313
+ refresh: () => fetchWorkload()
1214
1314
  };
1215
1315
  }
1216
1316
 
package/src/provider.tsx CHANGED
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import React, { createContext, useContext, useMemo } from 'react';
2
4
  import { CriticalPathClient, type ClientOptions } from '@critical-path/client';
3
5
 
@@ -1 +1 @@
1
- {"root":["./src/hooks.ts","./src/index.test.ts","./src/index.ts","./src/provider.tsx"],"version":"5.9.3"}
1
+ {"root":["./src/cancellation.test.tsx","./src/hooks.ts","./src/index.test.ts","./src/index.ts","./src/provider.tsx"],"version":"5.9.3"}