0
# Async Loading
1
2
RC Tree supports asynchronous data loading for lazy-loading tree nodes with loading states, error handling, and integration with virtual scrolling for large datasets.
3
4
## Capabilities
5
6
### Async Loading Configuration
7
8
Configure asynchronous data loading with comprehensive state management and event handling.
9
10
```typescript { .api }
11
/**
12
* Asynchronous loading configuration
13
*/
14
interface AsyncLoadingConfig<TreeDataType extends BasicDataNode = DataNode> {
15
/** Function to load data for a tree node */
16
loadData?: (treeNode: EventDataNode<TreeDataType>) => Promise<any>;
17
/** Keys of nodes that have completed loading */
18
loadedKeys?: Key[];
19
}
20
21
/**
22
* Load data function signature
23
*/
24
type LoadDataFunction<TreeDataType extends BasicDataNode = DataNode> = (
25
treeNode: EventDataNode<TreeDataType>
26
) => Promise<any>;
27
28
/**
29
* Load completion event handler
30
*/
31
interface LoadEventHandler<TreeDataType extends BasicDataNode = DataNode> {
32
onLoad?: (
33
loadedKeys: Key[],
34
info: {
35
event: 'load';
36
node: EventDataNode<TreeDataType>;
37
},
38
) => void;
39
}
40
```
41
42
### Loading States
43
44
Tree nodes automatically manage loading states during async operations.
45
46
```typescript { .api }
47
/**
48
* Node loading state properties
49
*/
50
interface NodeLoadingState {
51
/** Whether the node is currently loading */
52
loading?: boolean;
53
/** Whether the node has completed loading */
54
loaded?: boolean;
55
}
56
57
/**
58
* Enhanced event data node with loading state
59
*/
60
interface LoadingEventDataNode<TreeDataType> extends EventDataNode<TreeDataType> {
61
/** Current loading state */
62
loading: boolean;
63
/** Whether loading has completed */
64
loaded: boolean;
65
}
66
```
67
68
**Usage Examples:**
69
70
### Basic Async Loading
71
72
```typescript
73
import React, { useState } from "react";
74
import Tree from "rc-tree";
75
76
interface AsyncNodeData {
77
key: string;
78
title: string;
79
isLeaf?: boolean;
80
children?: AsyncNodeData[];
81
}
82
83
const BasicAsyncLoading = () => {
84
const [treeData, setTreeData] = useState<AsyncNodeData[]>([
85
{
86
key: '0-0',
87
title: 'Expandable Node 1',
88
},
89
{
90
key: '0-1',
91
title: 'Expandable Node 2',
92
},
93
{
94
key: '0-2',
95
title: 'Leaf Node',
96
isLeaf: true,
97
},
98
]);
99
const [loadedKeys, setLoadedKeys] = useState<string[]>([]);
100
101
const loadData = async (treeNode: any): Promise<void> => {
102
const { key } = treeNode;
103
104
console.log('Loading data for node:', key);
105
106
// Simulate API call delay
107
await new Promise(resolve => setTimeout(resolve, 1500));
108
109
// Simulate potential loading failure
110
if (Math.random() < 0.1) {
111
throw new Error(`Failed to load data for ${key}`);
112
}
113
114
// Generate child nodes
115
const children: AsyncNodeData[] = [
116
{
117
key: `${key}-0`,
118
title: `Child 1 of ${key}`,
119
isLeaf: true,
120
},
121
{
122
key: `${key}-1`,
123
title: `Child 2 of ${key}`,
124
},
125
{
126
key: `${key}-2`,
127
title: `Child 3 of ${key}`,
128
isLeaf: true,
129
},
130
];
131
132
// Update tree data
133
setTreeData(prevData => {
134
const updateNode = (nodes: AsyncNodeData[]): AsyncNodeData[] => {
135
return nodes.map(node => {
136
if (node.key === key) {
137
return { ...node, children };
138
}
139
if (node.children) {
140
return { ...node, children: updateNode(node.children) };
141
}
142
return node;
143
});
144
};
145
return updateNode(prevData);
146
});
147
148
// Mark as loaded
149
setLoadedKeys(prev => [...prev, key]);
150
};
151
152
return (
153
<Tree
154
prefixCls="rc-tree"
155
treeData={treeData}
156
loadData={loadData}
157
loadedKeys={loadedKeys}
158
onLoad={(keys, info) => {
159
console.log('Load completed:', keys, info);
160
}}
161
/>
162
);
163
};
164
```
165
166
### Async Loading with Error Handling
167
168
```typescript
169
import React, { useState } from "react";
170
import Tree from "rc-tree";
171
172
interface ErrorHandlingNodeData {
173
key: string;
174
title: string;
175
isLeaf?: boolean;
176
hasError?: boolean;
177
errorMessage?: string;
178
children?: ErrorHandlingNodeData[];
179
}
180
181
const AsyncLoadingWithErrorHandling = () => {
182
const [treeData, setTreeData] = useState<ErrorHandlingNodeData[]>([
183
{ key: '0-0', title: 'Reliable Node' },
184
{ key: '0-1', title: 'Unreliable Node (50% fail rate)' },
185
{ key: '0-2', title: 'Always Fails Node' },
186
]);
187
const [loadedKeys, setLoadedKeys] = useState<string[]>([]);
188
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
189
190
const loadData = async (treeNode: any): Promise<void> => {
191
const { key } = treeNode;
192
193
try {
194
// Simulate different failure rates
195
let failureRate = 0;
196
if (key.includes('0-1')) failureRate = 0.5;
197
if (key.includes('0-2')) failureRate = 1.0;
198
199
if (Math.random() < failureRate) {
200
throw new Error(`Network error loading ${key}`);
201
}
202
203
// Simulate loading delay
204
await new Promise(resolve => setTimeout(resolve, 1000));
205
206
// Generate children
207
const children: ErrorHandlingNodeData[] = [
208
{ key: `${key}-0`, title: `Child 1 of ${key}`, isLeaf: true },
209
{ key: `${key}-1`, title: `Child 2 of ${key}`, isLeaf: true },
210
];
211
212
// Update tree data with successful load
213
setTreeData(prevData => {
214
const updateNode = (nodes: ErrorHandlingNodeData[]): ErrorHandlingNodeData[] => {
215
return nodes.map(node => {
216
if (node.key === key) {
217
return {
218
...node,
219
children,
220
hasError: false,
221
errorMessage: undefined,
222
};
223
}
224
if (node.children) {
225
return { ...node, children: updateNode(node.children) };
226
}
227
return node;
228
});
229
};
230
return updateNode(prevData);
231
});
232
233
setLoadedKeys(prev => [...prev, key]);
234
235
} catch (error) {
236
console.error('Load failed:', error);
237
238
// Update tree data with error state
239
setTreeData(prevData => {
240
const updateNode = (nodes: ErrorHandlingNodeData[]): ErrorHandlingNodeData[] => {
241
return nodes.map(node => {
242
if (node.key === key) {
243
return {
244
...node,
245
hasError: true,
246
errorMessage: error instanceof Error ? error.message : 'Unknown error',
247
children: [],
248
};
249
}
250
if (node.children) {
251
return { ...node, children: updateNode(node.children) };
252
}
253
return node;
254
});
255
};
256
return updateNode(prevData);
257
});
258
259
// Still mark as "loaded" to prevent infinite retry
260
setLoadedKeys(prev => [...prev, key]);
261
}
262
};
263
264
const titleRender = (node: ErrorHandlingNodeData) => (
265
<span>
266
{node.title}
267
{node.hasError && (
268
<span style={{ color: 'red', marginLeft: 8 }}>
269
β ({node.errorMessage})
270
</span>
271
)}
272
</span>
273
);
274
275
return (
276
<Tree
277
prefixCls="rc-tree"
278
treeData={treeData}
279
loadData={loadData}
280
loadedKeys={loadedKeys}
281
expandedKeys={expandedKeys}
282
onExpand={setExpandedKeys}
283
titleRender={titleRender}
284
/>
285
);
286
};
287
```
288
289
### Async Loading with Caching
290
291
```typescript
292
import React, { useState, useCallback, useRef } from "react";
293
import Tree from "rc-tree";
294
295
interface CachedNodeData {
296
key: string;
297
title: string;
298
type: 'folder' | 'file';
299
isLeaf?: boolean;
300
children?: CachedNodeData[];
301
}
302
303
const AsyncLoadingWithCaching = () => {
304
const [treeData, setTreeData] = useState<CachedNodeData[]>([
305
{ key: 'root', title: 'Root Directory', type: 'folder' },
306
]);
307
const [loadedKeys, setLoadedKeys] = useState<string[]>([]);
308
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
309
310
// Cache to store loaded data
311
const cacheRef = useRef<Map<string, CachedNodeData[]>>(new Map());
312
313
const loadData = useCallback(async (treeNode: any): Promise<void> => {
314
const { key } = treeNode;
315
316
// Check cache first
317
const cachedData = cacheRef.current.get(key);
318
if (cachedData) {
319
console.log('Using cached data for:', key);
320
321
// Update tree with cached data
322
setTreeData(prevData => {
323
const updateNode = (nodes: CachedNodeData[]): CachedNodeData[] => {
324
return nodes.map(node => {
325
if (node.key === key) {
326
return { ...node, children: cachedData };
327
}
328
if (node.children) {
329
return { ...node, children: updateNode(node.children) };
330
}
331
return node;
332
});
333
};
334
return updateNode(prevData);
335
});
336
337
setLoadedKeys(prev => [...prev, key]);
338
return;
339
}
340
341
console.log('Loading fresh data for:', key);
342
343
// Simulate API call
344
await new Promise(resolve => setTimeout(resolve, 1200));
345
346
// Generate directory contents
347
const children: CachedNodeData[] = [
348
{ key: `${key}/documents`, title: 'Documents', type: 'folder' },
349
{ key: `${key}/images`, title: 'Images', type: 'folder' },
350
{ key: `${key}/readme.txt`, title: 'readme.txt', type: 'file', isLeaf: true },
351
{ key: `${key}/config.json`, title: 'config.json', type: 'file', isLeaf: true },
352
];
353
354
// Cache the loaded data
355
cacheRef.current.set(key, children);
356
357
// Update tree data
358
setTreeData(prevData => {
359
const updateNode = (nodes: CachedNodeData[]): CachedNodeData[] => {
360
return nodes.map(node => {
361
if (node.key === key) {
362
return { ...node, children };
363
}
364
if (node.children) {
365
return { ...node, children: updateNode(node.children) };
366
}
367
return node;
368
});
369
};
370
return updateNode(prevData);
371
});
372
373
setLoadedKeys(prev => [...prev, key]);
374
}, []);
375
376
const clearCache = () => {
377
cacheRef.current.clear();
378
setLoadedKeys([]);
379
setExpandedKeys([]);
380
setTreeData([{ key: 'root', title: 'Root Directory', type: 'folder' }]);
381
};
382
383
const titleRender = (node: CachedNodeData) => (
384
<span>
385
{node.type === 'folder' ? 'π' : 'π'} {node.title}
386
{cacheRef.current.has(node.key) && (
387
<span style={{ color: 'green', marginLeft: 8 }}>π¦</span>
388
)}
389
</span>
390
);
391
392
return (
393
<div>
394
<div style={{ marginBottom: 16 }}>
395
<button onClick={clearCache}>Clear Cache & Reset</button>
396
<p>Cache size: {cacheRef.current.size} items</p>
397
</div>
398
399
<Tree
400
prefixCls="rc-tree"
401
treeData={treeData}
402
loadData={loadData}
403
loadedKeys={loadedKeys}
404
expandedKeys={expandedKeys}
405
onExpand={setExpandedKeys}
406
titleRender={titleRender}
407
/>
408
</div>
409
);
410
};
411
```
412
413
### Async Loading with Search
414
415
```typescript
416
import React, { useState, useCallback, useMemo } from "react";
417
import Tree from "rc-tree";
418
419
interface SearchableNodeData {
420
key: string;
421
title: string;
422
description: string;
423
category: string;
424
isLeaf?: boolean;
425
children?: SearchableNodeData[];
426
}
427
428
const AsyncLoadingWithSearch = () => {
429
const [treeData, setTreeData] = useState<SearchableNodeData[]>([
430
{
431
key: 'products',
432
title: 'Products',
433
description: 'Product catalog',
434
category: 'root',
435
},
436
{
437
key: 'services',
438
title: 'Services',
439
description: 'Service offerings',
440
category: 'root',
441
},
442
]);
443
const [loadedKeys, setLoadedKeys] = useState<string[]>([]);
444
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
445
const [searchTerm, setSearchTerm] = useState('');
446
447
const loadData = useCallback(async (treeNode: any): Promise<void> => {
448
const { key } = treeNode;
449
450
// Simulate API search/load
451
await new Promise(resolve => setTimeout(resolve, 800));
452
453
let children: SearchableNodeData[] = [];
454
455
if (key === 'products') {
456
children = [
457
{ key: 'electronics', title: 'Electronics', description: 'Electronic devices and gadgets', category: 'product' },
458
{ key: 'books', title: 'Books', description: 'Physical and digital books', category: 'product' },
459
{ key: 'clothing', title: 'Clothing', description: 'Apparel and accessories', category: 'product' },
460
];
461
} else if (key === 'services') {
462
children = [
463
{ key: 'consulting', title: 'Consulting', description: 'Professional consulting services', category: 'service', isLeaf: true },
464
{ key: 'support', title: 'Support', description: 'Technical support services', category: 'service', isLeaf: true },
465
{ key: 'training', title: 'Training', description: 'Educational and training programs', category: 'service', isLeaf: true },
466
];
467
} else {
468
// Second level loading
469
children = Array.from({ length: 5 }, (_, index) => ({
470
key: `${key}-item-${index}`,
471
title: `${key} Item ${index + 1}`,
472
description: `Detailed description for ${key} item ${index + 1}`,
473
category: 'item',
474
isLeaf: true,
475
}));
476
}
477
478
// Filter children based on search term
479
if (searchTerm) {
480
children = children.filter(child =>
481
child.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
482
child.description.toLowerCase().includes(searchTerm.toLowerCase())
483
);
484
}
485
486
setTreeData(prevData => {
487
const updateNode = (nodes: SearchableNodeData[]): SearchableNodeData[] => {
488
return nodes.map(node => {
489
if (node.key === key) {
490
return { ...node, children };
491
}
492
if (node.children) {
493
return { ...node, children: updateNode(node.children) };
494
}
495
return node;
496
});
497
};
498
return updateNode(prevData);
499
});
500
501
setLoadedKeys(prev => [...prev, key]);
502
}, [searchTerm]);
503
504
// Re-load data when search term changes
505
const handleSearchChange = (value: string) => {
506
setSearchTerm(value);
507
// Clear loaded keys to force re-loading with new search criteria
508
setLoadedKeys([]);
509
setExpandedKeys([]);
510
};
511
512
const titleRender = (node: SearchableNodeData) => {
513
const highlightText = (text: string, highlight: string) => {
514
if (!highlight) return text;
515
516
const parts = text.split(new RegExp(`(${highlight})`, 'gi'));
517
return parts.map((part, index) =>
518
part.toLowerCase() === highlight.toLowerCase()
519
? <mark key={index}>{part}</mark>
520
: part
521
);
522
};
523
524
return (
525
<div>
526
<strong>{highlightText(node.title, searchTerm)}</strong>
527
<div style={{ fontSize: '0.8em', color: '#666' }}>
528
{highlightText(node.description, searchTerm)}
529
</div>
530
</div>
531
);
532
};
533
534
return (
535
<div>
536
<div style={{ marginBottom: 16 }}>
537
<input
538
type="text"
539
placeholder="Search items..."
540
value={searchTerm}
541
onChange={(e) => handleSearchChange(e.target.value)}
542
style={{ width: '100%', padding: 8 }}
543
/>
544
</div>
545
546
<Tree
547
prefixCls="rc-tree"
548
treeData={treeData}
549
loadData={loadData}
550
loadedKeys={loadedKeys}
551
expandedKeys={expandedKeys}
552
onExpand={setExpandedKeys}
553
titleRender={titleRender}
554
itemHeight={48} // Taller for two-line content
555
/>
556
</div>
557
);
558
};
559
```
560
561
## Advanced Async Loading Patterns
562
563
### Lazy Loading with Pagination
564
565
```typescript
566
import React, { useState, useCallback } from "react";
567
import Tree from "rc-tree";
568
569
interface PaginatedNodeData {
570
key: string;
571
title: string;
572
hasMore?: boolean;
573
page?: number;
574
isLeaf?: boolean;
575
children?: PaginatedNodeData[];
576
}
577
578
const PaginatedAsyncLoading = () => {
579
const [treeData, setTreeData] = useState<PaginatedNodeData[]>([
580
{ key: 'dataset-1', title: 'Large Dataset 1 (1000+ items)', hasMore: true, page: 0 },
581
{ key: 'dataset-2', title: 'Large Dataset 2 (500+ items)', hasMore: true, page: 0 },
582
]);
583
const [loadedKeys, setLoadedKeys] = useState<string[]>([]);
584
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
585
586
const loadData = useCallback(async (treeNode: any): Promise<void> => {
587
const { key } = treeNode;
588
589
// Find the node to get current page
590
const findNode = (nodes: PaginatedNodeData[], targetKey: string): PaginatedNodeData | null => {
591
for (const node of nodes) {
592
if (node.key === targetKey) return node;
593
if (node.children) {
594
const found = findNode(node.children, targetKey);
595
if (found) return found;
596
}
597
}
598
return null;
599
};
600
601
const nodeData = findNode(treeData, key);
602
const currentPage = nodeData?.page || 0;
603
const pageSize = 20;
604
605
console.log(`Loading page ${currentPage + 1} for ${key}`);
606
607
// Simulate paginated API call
608
await new Promise(resolve => setTimeout(resolve, 1000));
609
610
// Generate page of data
611
const startIndex = currentPage * pageSize;
612
const pageItems: PaginatedNodeData[] = Array.from({ length: pageSize }, (_, index) => ({
613
key: `${key}-item-${startIndex + index}`,
614
title: `Item ${startIndex + index + 1}`,
615
isLeaf: true,
616
}));
617
618
// Add "Load More" item if there are more pages
619
const totalItems = key === 'dataset-1' ? 1000 : 500;
620
const hasMore = (startIndex + pageSize) < totalItems;
621
622
if (hasMore) {
623
pageItems.push({
624
key: `${key}-load-more-${currentPage + 1}`,
625
title: `π Load More... (${totalItems - startIndex - pageSize} remaining)`,
626
hasMore: true,
627
page: currentPage + 1,
628
});
629
}
630
631
setTreeData(prevData => {
632
const updateNode = (nodes: PaginatedNodeData[]): PaginatedNodeData[] => {
633
return nodes.map(node => {
634
if (node.key === key) {
635
const existingChildren = node.children || [];
636
return {
637
...node,
638
children: [...existingChildren, ...pageItems],
639
hasMore: false, // This node itself no longer needs loading
640
};
641
}
642
if (node.children) {
643
return { ...node, children: updateNode(node.children) };
644
}
645
return node;
646
});
647
};
648
return updateNode(prevData);
649
});
650
651
setLoadedKeys(prev => [...prev, key]);
652
}, [treeData]);
653
654
return (
655
<Tree
656
prefixCls="rc-tree"
657
treeData={treeData}
658
loadData={loadData}
659
loadedKeys={loadedKeys}
660
expandedKeys={expandedKeys}
661
onExpand={setExpandedKeys}
662
/>
663
);
664
};
665
```
666
667
### Real-time Data Loading
668
669
```typescript
670
import React, { useState, useCallback, useEffect, useRef } from "react";
671
import Tree from "rc-tree";
672
673
interface RealtimeNodeData {
674
key: string;
675
title: string;
676
lastUpdated?: Date;
677
isLive?: boolean;
678
children?: RealtimeNodeData[];
679
}
680
681
const RealtimeAsyncLoading = () => {
682
const [treeData, setTreeData] = useState<RealtimeNodeData[]>([
683
{ key: 'live-feeds', title: 'π‘ Live Data Feeds', isLive: true },
684
{ key: 'static-data', title: 'π Static Data' },
685
]);
686
const [loadedKeys, setLoadedKeys] = useState<string[]>([]);
687
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
688
const intervalsRef = useRef<Map<string, NodeJS.Timeout>>(new Map());
689
690
const loadData = useCallback(async (treeNode: any): Promise<void> => {
691
const { key } = treeNode;
692
693
await new Promise(resolve => setTimeout(resolve, 500));
694
695
let children: RealtimeNodeData[] = [];
696
697
if (key === 'live-feeds') {
698
children = [
699
{ key: 'sensor-1', title: 'Temperature Sensor', isLive: true, lastUpdated: new Date() },
700
{ key: 'sensor-2', title: 'Humidity Sensor', isLive: true, lastUpdated: new Date() },
701
{ key: 'sensor-3', title: 'Pressure Sensor', isLive: true, lastUpdated: new Date() },
702
];
703
704
// Start real-time updates for live nodes
705
children.forEach(child => {
706
if (child.isLive && !intervalsRef.current.has(child.key)) {
707
const interval = setInterval(() => {
708
setTreeData(prevData => {
709
const updateNode = (nodes: RealtimeNodeData[]): RealtimeNodeData[] => {
710
return nodes.map(node => {
711
if (node.key === child.key) {
712
return {
713
...node,
714
lastUpdated: new Date(),
715
title: `${child.title.split(' (')[0]} (${Math.random().toFixed(2)})`,
716
};
717
}
718
if (node.children) {
719
return { ...node, children: updateNode(node.children) };
720
}
721
return node;
722
});
723
};
724
return updateNode(prevData);
725
});
726
}, 2000);
727
728
intervalsRef.current.set(child.key, interval);
729
}
730
});
731
} else {
732
children = [
733
{ key: 'static-1', title: 'Static File 1', lastUpdated: new Date('2024-01-01') },
734
{ key: 'static-2', title: 'Static File 2', lastUpdated: new Date('2024-01-02') },
735
];
736
}
737
738
setTreeData(prevData => {
739
const updateNode = (nodes: RealtimeNodeData[]): RealtimeNodeData[] => {
740
return nodes.map(node => {
741
if (node.key === key) {
742
return { ...node, children };
743
}
744
if (node.children) {
745
return { ...node, children: updateNode(node.children) };
746
}
747
return node;
748
});
749
};
750
return updateNode(prevData);
751
});
752
753
setLoadedKeys(prev => [...prev, key]);
754
}, []);
755
756
// Cleanup intervals on unmount
757
useEffect(() => {
758
return () => {
759
intervalsRef.current.forEach(interval => clearInterval(interval));
760
intervalsRef.current.clear();
761
};
762
}, []);
763
764
const titleRender = (node: RealtimeNodeData) => (
765
<span>
766
{node.title}
767
{node.isLive && <span style={{ color: 'green' }}> π΄</span>}
768
{node.lastUpdated && (
769
<span style={{ fontSize: '0.8em', color: '#666', marginLeft: 8 }}>
770
{node.lastUpdated.toLocaleTimeString()}
771
</span>
772
)}
773
</span>
774
);
775
776
return (
777
<div>
778
<h3>Real-time Data Tree</h3>
779
<p>Live sensors update every 2 seconds</p>
780
781
<Tree
782
prefixCls="rc-tree"
783
treeData={treeData}
784
loadData={loadData}
785
loadedKeys={loadedKeys}
786
expandedKeys={expandedKeys}
787
onExpand={setExpandedKeys}
788
titleRender={titleRender}
789
/>
790
</div>
791
);
792
};
793
```
794
795
## Best Practices
796
797
### Error Boundaries for Async Loading
798
799
```typescript
800
import React, { Component, ReactNode } from "react";
801
802
interface ErrorBoundaryState {
803
hasError: boolean;
804
error?: Error;
805
}
806
807
class AsyncTreeErrorBoundary extends Component<
808
{ children: ReactNode },
809
ErrorBoundaryState
810
> {
811
constructor(props: { children: ReactNode }) {
812
super(props);
813
this.state = { hasError: false };
814
}
815
816
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
817
return { hasError: true, error };
818
}
819
820
componentDidCatch(error: Error, errorInfo: any) {
821
console.error('Tree async loading error:', error, errorInfo);
822
}
823
824
render() {
825
if (this.state.hasError) {
826
return (
827
<div style={{ padding: 16, border: '1px solid #ff4d4f', borderRadius: 4 }}>
828
<h4>Tree Loading Error</h4>
829
<p>Something went wrong while loading tree data.</p>
830
<button onClick={() => this.setState({ hasError: false })}>
831
Try Again
832
</button>
833
</div>
834
);
835
}
836
837
return this.props.children;
838
}
839
}
840
841
// Usage example
842
const SafeAsyncTree = () => (
843
<AsyncTreeErrorBoundary>
844
<AsyncLoadingTree />
845
</AsyncTreeErrorBoundary>
846
);
847
```