@startsimpli/funnels 0.1.3 → 0.1.5
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/package.json +9 -31
- package/src/api/README.md +507 -0
- package/src/api/adapter.ts +106 -0
- package/src/api/client.test.ts +640 -0
- package/src/api/client.ts +385 -0
- package/src/api/default-adapter.ts +243 -0
- package/src/api/index.ts +24 -0
- package/src/components/FilterRuleEditor/ARCHITECTURE.md +354 -0
- package/src/components/FilterRuleEditor/FieldSelector.tsx +91 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +462 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +520 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +225 -0
- package/src/components/FilterRuleEditor/LogicToggle.tsx +64 -0
- package/src/components/FilterRuleEditor/OperatorSelector.tsx +75 -0
- package/src/components/FilterRuleEditor/README.md +291 -0
- package/src/components/FilterRuleEditor/RuleRow.tsx +246 -0
- package/src/components/FilterRuleEditor/ValueInputs/BooleanValueInput.tsx +54 -0
- package/src/components/FilterRuleEditor/ValueInputs/ChoiceValueInput.tsx +83 -0
- package/src/components/FilterRuleEditor/ValueInputs/DateValueInput.tsx +70 -0
- package/src/components/FilterRuleEditor/ValueInputs/MultiChoiceValueInput.tsx +132 -0
- package/src/components/FilterRuleEditor/ValueInputs/NumberValueInput.tsx +73 -0
- package/src/components/FilterRuleEditor/ValueInputs/TextValueInput.tsx +50 -0
- package/src/components/FilterRuleEditor/ValueInputs/index.ts +12 -0
- package/src/components/FilterRuleEditor/constants.ts +64 -0
- package/src/components/FilterRuleEditor/index.ts +14 -0
- package/src/components/FunnelCard/DESIGN.md +447 -0
- package/src/components/FunnelCard/FunnelCard.stories.tsx +484 -0
- package/src/components/FunnelCard/FunnelCard.test.ts +257 -0
- package/src/components/FunnelCard/FunnelCard.test.tsx +336 -0
- package/src/components/FunnelCard/FunnelCard.tsx +204 -0
- package/src/components/FunnelCard/FunnelStats.tsx +68 -0
- package/src/components/FunnelCard/IMPLEMENTATION_SUMMARY.md +505 -0
- package/src/components/FunnelCard/INSTALLATION.md +304 -0
- package/src/components/FunnelCard/MatchBar.tsx +49 -0
- package/src/components/FunnelCard/README.md +294 -0
- package/src/components/FunnelCard/StageIndicator.tsx +62 -0
- package/src/components/FunnelCard/StatusBadge.tsx +52 -0
- package/src/components/FunnelCard/index.ts +14 -0
- package/src/components/FunnelPreview/EntityCard.tsx +72 -0
- package/src/components/FunnelPreview/FunnelPreview.stories.tsx +227 -0
- package/src/components/FunnelPreview/FunnelPreview.test.tsx +316 -0
- package/src/components/FunnelPreview/FunnelPreview.tsx +249 -0
- package/src/components/FunnelPreview/LoadingPreview.tsx +60 -0
- package/src/components/FunnelPreview/PreviewStats.tsx +78 -0
- package/src/components/FunnelPreview/README.md +337 -0
- package/src/components/FunnelPreview/StageBreakdown.tsx +94 -0
- package/src/components/FunnelPreview/example.tsx +286 -0
- package/src/components/FunnelPreview/index.ts +14 -0
- package/src/components/FunnelRunHistory/COMPONENT_SUMMARY.md +246 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +272 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +323 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +329 -0
- package/src/components/FunnelRunHistory/README.md +325 -0
- package/src/components/FunnelRunHistory/RunActions.tsx +168 -0
- package/src/components/FunnelRunHistory/RunDetailsModal.tsx +221 -0
- package/src/components/FunnelRunHistory/RunFilters.tsx +128 -0
- package/src/components/FunnelRunHistory/RunRow.tsx +122 -0
- package/src/components/FunnelRunHistory/RunStatusBadge.tsx +75 -0
- package/src/components/FunnelRunHistory/StageBreakdownList.tsx +110 -0
- package/src/components/FunnelRunHistory/index.ts +51 -0
- package/src/components/FunnelRunHistory/types.ts +40 -0
- package/src/components/FunnelRunHistory/utils.test.ts +126 -0
- package/src/components/FunnelRunHistory/utils.ts +100 -0
- package/src/components/FunnelStageBuilder/AddStageButton.tsx +52 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.css +413 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +312 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +304 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +321 -0
- package/src/components/FunnelStageBuilder/README.md +341 -0
- package/src/components/FunnelStageBuilder/StageActions.test.tsx +205 -0
- package/src/components/FunnelStageBuilder/StageActions.tsx +126 -0
- package/src/components/FunnelStageBuilder/StageCard.tsx +202 -0
- package/src/components/FunnelStageBuilder/StageForm.tsx +262 -0
- package/src/components/FunnelStageBuilder/TagInput.test.tsx +178 -0
- package/src/components/FunnelStageBuilder/TagInput.tsx +129 -0
- package/src/components/FunnelStageBuilder/index.ts +21 -0
- package/src/components/FunnelVisualFlow/FlowLegend.tsx +77 -0
- package/{dist/components/index.css → src/components/FunnelVisualFlow/FunnelVisualFlow.css} +89 -13
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +254 -0
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +208 -0
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +229 -0
- package/src/components/FunnelVisualFlow/README.md +323 -0
- package/src/components/FunnelVisualFlow/StageNode.tsx +188 -0
- package/src/components/FunnelVisualFlow/example.tsx +227 -0
- package/src/components/FunnelVisualFlow/index.ts +10 -0
- package/src/components/index.ts +102 -0
- package/src/core/README.md +307 -0
- package/src/core/engine.test.ts +1087 -0
- package/src/core/engine.ts +329 -0
- package/src/core/evaluator.example.ts +353 -0
- package/src/core/evaluator.test.ts +639 -0
- package/src/core/evaluator.ts +261 -0
- package/src/core/field-resolver.example.ts +175 -0
- package/src/core/field-resolver.test.ts +541 -0
- package/src/core/field-resolver.ts +247 -0
- package/src/core/index.ts +34 -0
- package/src/core/operators.test.ts +539 -0
- package/src/core/operators.ts +241 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useDebouncedValue.ts +28 -0
- package/src/index.ts +155 -0
- package/src/store/README.md +342 -0
- package/src/store/create-funnel-store.test.ts +686 -0
- package/src/store/create-funnel-store.ts +538 -0
- package/src/store/index.ts +9 -0
- package/src/store/types.ts +294 -0
- package/src/stories/CrossDomain.stories.tsx +149 -0
- package/src/stories/Welcome.stories.tsx +81 -0
- package/src/stories/demo-data/index.ts +3 -0
- package/src/stories/demo-data/investors.ts +216 -0
- package/src/stories/demo-data/leads.ts +223 -0
- package/src/stories/demo-data/recipes.ts +217 -0
- package/src/test/setup.ts +5 -0
- package/src/types/index.ts +843 -0
- package/dist/client-3ESO2NHy.d.ts +0 -310
- package/dist/client-CZu03ACp.d.cts +0 -310
- package/dist/components/index.cjs +0 -3243
- package/dist/components/index.cjs.map +0 -1
- package/dist/components/index.css.map +0 -1
- package/dist/components/index.d.cts +0 -726
- package/dist/components/index.d.ts +0 -726
- package/dist/components/index.js +0 -3196
- package/dist/components/index.js.map +0 -1
- package/dist/core/index.cjs +0 -500
- package/dist/core/index.cjs.map +0 -1
- package/dist/core/index.d.cts +0 -359
- package/dist/core/index.d.ts +0 -359
- package/dist/core/index.js +0 -486
- package/dist/core/index.js.map +0 -1
- package/dist/hooks/index.cjs +0 -21
- package/dist/hooks/index.cjs.map +0 -1
- package/dist/hooks/index.d.cts +0 -11
- package/dist/hooks/index.d.ts +0 -11
- package/dist/hooks/index.js +0 -19
- package/dist/hooks/index.js.map +0 -1
- package/dist/index-BGDEXbuz.d.cts +0 -434
- package/dist/index-BGDEXbuz.d.ts +0 -434
- package/dist/index.cjs +0 -4499
- package/dist/index.cjs.map +0 -1
- package/dist/index.css +0 -198
- package/dist/index.css.map +0 -1
- package/dist/index.d.cts +0 -99
- package/dist/index.d.ts +0 -99
- package/dist/index.js +0 -4421
- package/dist/index.js.map +0 -1
- package/dist/store/index.cjs +0 -391
- package/dist/store/index.cjs.map +0 -1
- package/dist/store/index.d.cts +0 -225
- package/dist/store/index.d.ts +0 -225
- package/dist/store/index.js +0 -388
- package/dist/store/index.js.map +0 -1
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# Funnel Store
|
|
2
|
+
|
|
3
|
+
Zustand state management for @simpli/funnels.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Factory Pattern** - Create isolated store instances for multi-editor scenarios
|
|
8
|
+
- **Optimistic Updates** - UI updates immediately, rollback on error
|
|
9
|
+
- **Dirty Tracking** - Track unsaved changes
|
|
10
|
+
- **Loading States** - Better UX with loading indicators
|
|
11
|
+
- **Error Handling** - Automatic error capture and rollback
|
|
12
|
+
- **Generic** - Works with any entity type (no product-specific logic)
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
### Basic Setup
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { createFunnelStore } from '@simpli/funnels';
|
|
20
|
+
import { FetchAdapter, FunnelApiClient } from '@simpli/funnels';
|
|
21
|
+
|
|
22
|
+
// Create API client
|
|
23
|
+
const adapter = new FetchAdapter({
|
|
24
|
+
headers: {
|
|
25
|
+
'Authorization': 'Bearer your-token',
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const apiClient = new FunnelApiClient(adapter, 'https://api.example.com');
|
|
30
|
+
|
|
31
|
+
// Create store instance
|
|
32
|
+
const useFunnelStore = createFunnelStore(apiClient);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### In React Components
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
function FunnelEditor() {
|
|
39
|
+
const {
|
|
40
|
+
funnels,
|
|
41
|
+
selectedFunnel,
|
|
42
|
+
isLoading,
|
|
43
|
+
error,
|
|
44
|
+
loadFunnels,
|
|
45
|
+
selectFunnel,
|
|
46
|
+
updateFunnel,
|
|
47
|
+
} = useFunnelStore();
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
loadFunnels({ status: 'active' });
|
|
51
|
+
}, [loadFunnels]);
|
|
52
|
+
|
|
53
|
+
const handleSave = async () => {
|
|
54
|
+
if (!selectedFunnel) return;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await updateFunnel(selectedFunnel.id, {
|
|
58
|
+
name: 'Updated Name',
|
|
59
|
+
});
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// Error handled automatically, UI rolled back
|
|
62
|
+
console.error('Failed to save:', error);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (isLoading) return <div>Loading...</div>;
|
|
67
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div>
|
|
71
|
+
{funnels.map(funnel => (
|
|
72
|
+
<div key={funnel.id} onClick={() => selectFunnel(funnel.id)}>
|
|
73
|
+
{funnel.name}
|
|
74
|
+
</div>
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Multiple Store Instances
|
|
82
|
+
|
|
83
|
+
The factory pattern allows multiple independent stores:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// Editor 1
|
|
87
|
+
const editor1ApiClient = new FunnelApiClient(adapter1, baseUrl);
|
|
88
|
+
const useEditor1Store = createFunnelStore(editor1ApiClient);
|
|
89
|
+
|
|
90
|
+
// Editor 2
|
|
91
|
+
const editor2ApiClient = new FunnelApiClient(adapter2, baseUrl);
|
|
92
|
+
const useEditor2Store = createFunnelStore(editor2ApiClient);
|
|
93
|
+
|
|
94
|
+
// Each store is completely isolated
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## API
|
|
98
|
+
|
|
99
|
+
### State
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
interface FunnelStore<TEntity = any> {
|
|
103
|
+
// Data
|
|
104
|
+
funnels: Funnel<TEntity>[];
|
|
105
|
+
selectedFunnel: Funnel<TEntity> | null;
|
|
106
|
+
selectedStage: FunnelStage<TEntity> | null;
|
|
107
|
+
runs: FunnelRun[];
|
|
108
|
+
pagination: { count, next, previous, currentPage, pageSize };
|
|
109
|
+
|
|
110
|
+
// UI State
|
|
111
|
+
isLoading: boolean;
|
|
112
|
+
error: Error | null;
|
|
113
|
+
isDirty: boolean;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Actions
|
|
118
|
+
|
|
119
|
+
#### Funnel Management
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// Load funnels with filters
|
|
123
|
+
await loadFunnels({ status: 'active', page: 1 });
|
|
124
|
+
|
|
125
|
+
// Select funnel for editing
|
|
126
|
+
selectFunnel('funnel-id');
|
|
127
|
+
|
|
128
|
+
// Create new funnel
|
|
129
|
+
const funnel = await createFunnel({
|
|
130
|
+
name: 'My Funnel',
|
|
131
|
+
status: 'draft',
|
|
132
|
+
input_type: 'contacts',
|
|
133
|
+
stages: [],
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Update funnel (optimistic)
|
|
137
|
+
await updateFunnel('funnel-id', {
|
|
138
|
+
name: 'Updated Name',
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Delete funnel (optimistic)
|
|
142
|
+
await deleteFunnel('funnel-id');
|
|
143
|
+
|
|
144
|
+
// Duplicate funnel
|
|
145
|
+
const copy = await duplicateFunnel('funnel-id');
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### Stage Management
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// Select stage
|
|
152
|
+
selectStage('stage-id');
|
|
153
|
+
|
|
154
|
+
// Create stage
|
|
155
|
+
const stage = await createStage('funnel-id', {
|
|
156
|
+
order: 0,
|
|
157
|
+
name: 'Qualification',
|
|
158
|
+
filter_logic: 'AND',
|
|
159
|
+
rules: [],
|
|
160
|
+
match_action: 'continue',
|
|
161
|
+
no_match_action: 'exclude',
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Update stage (optimistic)
|
|
165
|
+
await updateStage('funnel-id', 'stage-id', {
|
|
166
|
+
name: 'Updated Name',
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Delete stage (optimistic)
|
|
170
|
+
await deleteStage('funnel-id', 'stage-id');
|
|
171
|
+
|
|
172
|
+
// Reorder stages (optimistic)
|
|
173
|
+
await reorderStages('funnel-id', ['stage-2', 'stage-1', 'stage-3']);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### Run Operations
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
// Run funnel
|
|
180
|
+
const run = await runFunnel('funnel-id', {
|
|
181
|
+
trigger_type: 'manual',
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Load run history
|
|
185
|
+
await loadRuns('funnel-id', {
|
|
186
|
+
status: 'completed',
|
|
187
|
+
page: 1,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Cancel running funnel
|
|
191
|
+
await cancelRun('run-id');
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
#### UI State
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
// Set dirty flag
|
|
198
|
+
setDirty(true);
|
|
199
|
+
|
|
200
|
+
// Clear error
|
|
201
|
+
clearError();
|
|
202
|
+
|
|
203
|
+
// Reset store
|
|
204
|
+
reset();
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Optimistic Updates
|
|
208
|
+
|
|
209
|
+
The store automatically implements optimistic updates for:
|
|
210
|
+
- `updateFunnel` - Update funnel data
|
|
211
|
+
- `deleteFunnel` - Remove funnel
|
|
212
|
+
- `updateStage` - Update stage data
|
|
213
|
+
- `deleteStage` - Remove stage
|
|
214
|
+
- `reorderStages` - Reorder stages
|
|
215
|
+
|
|
216
|
+
On error, the store automatically rolls back to the previous state.
|
|
217
|
+
|
|
218
|
+
### How It Works
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// 1. User clicks save
|
|
222
|
+
await updateFunnel('funnel-1', { name: 'New Name' });
|
|
223
|
+
|
|
224
|
+
// 2. UI updates immediately (optimistic)
|
|
225
|
+
// -> funnels[0].name = 'New Name'
|
|
226
|
+
|
|
227
|
+
// 3. API call happens in background
|
|
228
|
+
// -> POST /api/v1/funnels/funnel-1/
|
|
229
|
+
|
|
230
|
+
// 4a. Success: Update with server response
|
|
231
|
+
// -> funnels[0] = serverResponse
|
|
232
|
+
|
|
233
|
+
// 4b. Error: Rollback to previous state
|
|
234
|
+
// -> funnels[0].name = 'Old Name'
|
|
235
|
+
// -> error = Error('Update failed')
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Dirty Tracking
|
|
239
|
+
|
|
240
|
+
Track unsaved changes:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
const { isDirty, setDirty } = useFunnelStore();
|
|
244
|
+
|
|
245
|
+
// Set dirty when user makes changes
|
|
246
|
+
const handleChange = (value) => {
|
|
247
|
+
setDirty(true);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// Clear dirty on save
|
|
251
|
+
const handleSave = async () => {
|
|
252
|
+
await updateFunnel(selectedFunnel.id, data);
|
|
253
|
+
// isDirty automatically set to false
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Warn on navigation
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
const handleBeforeUnload = (e) => {
|
|
259
|
+
if (isDirty) {
|
|
260
|
+
e.preventDefault();
|
|
261
|
+
e.returnValue = '';
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
266
|
+
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
267
|
+
}, [isDirty]);
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Error Handling
|
|
271
|
+
|
|
272
|
+
Errors are automatically captured and stored:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
const { error, clearError } = useFunnelStore();
|
|
276
|
+
|
|
277
|
+
// Errors set automatically
|
|
278
|
+
await updateFunnel('bad-id', { name: 'New' });
|
|
279
|
+
// -> error = Error('Not found')
|
|
280
|
+
|
|
281
|
+
// Clear error manually
|
|
282
|
+
clearError();
|
|
283
|
+
|
|
284
|
+
// Or display in UI
|
|
285
|
+
{error && <Alert>{error.message}</Alert>}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Testing
|
|
289
|
+
|
|
290
|
+
See `create-funnel-store.test.ts` for comprehensive test examples.
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { createFunnelStore } from '@simpli/funnels';
|
|
294
|
+
import { vi } from 'vitest';
|
|
295
|
+
|
|
296
|
+
// Mock API client
|
|
297
|
+
const mockClient = {
|
|
298
|
+
listFunnels: vi.fn(),
|
|
299
|
+
createFunnel: vi.fn(),
|
|
300
|
+
// ... other methods
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Create store with mock
|
|
304
|
+
const useFunnelStore = createFunnelStore(mockClient);
|
|
305
|
+
|
|
306
|
+
// Test actions
|
|
307
|
+
const { loadFunnels } = useFunnelStore.getState();
|
|
308
|
+
await loadFunnels();
|
|
309
|
+
|
|
310
|
+
expect(mockClient.listFunnels).toHaveBeenCalled();
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## TypeScript
|
|
314
|
+
|
|
315
|
+
The store is fully typed with generics:
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
interface InvestorEntity {
|
|
319
|
+
id: string;
|
|
320
|
+
name: string;
|
|
321
|
+
stage: string;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const useFunnelStore = createFunnelStore<InvestorEntity>(apiClient);
|
|
325
|
+
|
|
326
|
+
// TypeScript knows entity type
|
|
327
|
+
const { funnels } = useFunnelStore();
|
|
328
|
+
// funnels: Funnel<InvestorEntity>[]
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Performance
|
|
332
|
+
|
|
333
|
+
- **Selective Rendering** - Use Zustand selectors to prevent unnecessary re-renders
|
|
334
|
+
- **Pagination** - Load funnels in pages
|
|
335
|
+
- **Debounced Updates** - Debounce rapid updates
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
// Selective rendering
|
|
339
|
+
const selectedFunnel = useFunnelStore(state => state.selectedFunnel);
|
|
340
|
+
|
|
341
|
+
// Only re-renders when selectedFunnel changes
|
|
342
|
+
```
|