|
| 1 | +# FCL API Design Discussion: Instance vs Context-Passing |
| 2 | + |
| 3 | +**Date:** July 24, 2025 |
| 4 | +**Context:** Migration away from global state, evaluating instance-based vs context-passing patterns |
| 5 | + |
| 6 | +## Background |
| 7 | + |
| 8 | +FCL was migrated from global state to an instance-based approach: |
| 9 | + |
| 10 | +```javascript |
| 11 | +// Old global approach |
| 12 | +mutate(transaction) |
| 13 | + |
| 14 | +// New instance approach |
| 15 | +const fcl = createFCL(config) |
| 16 | +fcl.mutate(transaction) |
| 17 | +``` |
| 18 | + |
| 19 | +The global functions still exist for backward compatibility, but now there are also context-bound functions in the instance. |
| 20 | + |
| 21 | +## Question: Was this the right move vs tree-shakable context-passing? |
| 22 | + |
| 23 | +## Option 1: Instance-Based Approach (Current) |
| 24 | + |
| 25 | +```javascript |
| 26 | +import { createFCL } from 'fcl-js' |
| 27 | +const fcl = createFCL(config) |
| 28 | +fcl.mutate(params) |
| 29 | +fcl.query(script) |
| 30 | +fcl.authenticate() |
| 31 | +``` |
| 32 | + |
| 33 | +**Benefits:** |
| 34 | +- ✅ Better developer experience (DX) |
| 35 | +- ✅ Cleaner, more intuitive API |
| 36 | +- ✅ Context binding prevents errors |
| 37 | +- ✅ Better IDE autocomplete/TypeScript support |
| 38 | +- ✅ Backward compatibility maintained |
| 39 | +- ✅ Methods are discoverable on the instance |
| 40 | + |
| 41 | +**Drawbacks:** |
| 42 | +- ❌ Potentially larger bundles (imports entire instance) |
| 43 | +- ❌ Less tree-shakable |
| 44 | + |
| 45 | +## Option 2: Context-Passing Functions |
| 46 | + |
| 47 | +```javascript |
| 48 | +import { mutate, query, authenticate, createContext } from 'fcl-js' |
| 49 | +const context = createContext(config) |
| 50 | +mutate(context, params) |
| 51 | +query(context, script) |
| 52 | +authenticate(context) |
| 53 | +``` |
| 54 | + |
| 55 | +**Benefits:** |
| 56 | +- ✅ Better tree-shaking (only import what you use) |
| 57 | +- ✅ Functional programming style |
| 58 | +- ✅ Better bundle size optimization |
| 59 | + |
| 60 | +**Drawbacks:** |
| 61 | +- ❌ More verbose API |
| 62 | +- ❌ Easy to forget context parameter |
| 63 | +- ❌ Context gets passed everywhere |
| 64 | +- ❌ Harder to discover available methods |
| 65 | +- ❌ More imports needed |
| 66 | + |
| 67 | +## Option 3: Overloaded Functions (Hybrid) |
| 68 | + |
| 69 | +Support both patterns with function overloads: |
| 70 | + |
| 71 | +```javascript |
| 72 | +export function mutate(contextOrFirstArg, ...args) { |
| 73 | + // Check if first arg is a context object |
| 74 | + if (contextOrFirstArg && typeof contextOrFirstArg === 'object' && contextOrFirstArg._isContext) { |
| 75 | + // New API: mutate(context, ...params) |
| 76 | + return mutateFn(contextOrFirstArg, ...args) |
| 77 | + } else { |
| 78 | + // Legacy API: mutate(...params) - use global context |
| 79 | + return mutateFn(getGlobalContext(), contextOrFirstArg, ...args) |
| 80 | + } |
| 81 | +} |
| 82 | +``` |
| 83 | + |
| 84 | +**Usage Examples:** |
| 85 | +```javascript |
| 86 | +// Legacy (still works) |
| 87 | +import { mutate } from 'fcl-js' |
| 88 | +mutate(transaction, options) |
| 89 | + |
| 90 | +// New context-passing (tree-shakable) |
| 91 | +import { mutate, createContext } from 'fcl-js' |
| 92 | +const context = createContext(config) |
| 93 | +mutate(context, transaction, options) |
| 94 | + |
| 95 | +// Instance-based (also still works) |
| 96 | +const fcl = createFCL(config) |
| 97 | +fcl.mutate(transaction, options) |
| 98 | +``` |
| 99 | + |
| 100 | +**Benefits:** |
| 101 | +- ✅ Zero breaking changes |
| 102 | +- ✅ Tree-shakable for new users |
| 103 | +- ✅ Clear migration path |
| 104 | +- ✅ Single function handles both patterns |
| 105 | +- ✅ Single API surface |
| 106 | + |
| 107 | +## Option 4: Both Patterns Side-by-Side |
| 108 | + |
| 109 | +```javascript |
| 110 | +// Instance-based API |
| 111 | +export function createFCL(config) { |
| 112 | + const context = createContext(config) |
| 113 | + |
| 114 | + return { |
| 115 | + mutate: (...params) => mutate(context, ...params), |
| 116 | + query: (...params) => query(context, ...params), |
| 117 | + authenticate: (...params) => authenticate(context, ...params), |
| 118 | + } |
| 119 | +} |
| 120 | + |
| 121 | +// Context-passing API (tree-shakable) |
| 122 | +export { mutate, query, authenticate } from './core' |
| 123 | +export { createContext } from './context' |
| 124 | +``` |
| 125 | + |
| 126 | +## Analysis & Recommendation |
| 127 | + |
| 128 | +### Tree-Shaking Considerations |
| 129 | +- **Context-passing** is more tree-shakable |
| 130 | +- **Instance-based** potentially bundles all methods |
| 131 | +- For SDK-style libraries like FCL, users typically use multiple functions anyway |
| 132 | + |
| 133 | +### Developer Experience (DX) |
| 134 | +- **Instance-based** has significantly better DX |
| 135 | +- **Context-passing** is more verbose and error-prone |
| 136 | +- IDE support and discoverability favor instance approach |
| 137 | + |
| 138 | +### Library Comparisons |
| 139 | +- **React Query**: Uses hooks + context provider pattern |
| 140 | +- **Zustand**: Primarily instance-based, supports context for SSR |
| 141 | +- **Jotai**: Supports both global and explicit store patterns |
| 142 | + |
| 143 | +### Final Assessment |
| 144 | + |
| 145 | +**For FCL specifically:** |
| 146 | + |
| 147 | +1. **Instance-based approach is better for DX** - the primary consideration for an SDK |
| 148 | +2. **Context-passing has minimal tree-shaking benefit** since FCL users typically use multiple functions |
| 149 | +3. **Overloaded functions provide best of both worlds** but add complexity |
| 150 | +4. **Current instance approach strikes the right balance** |
| 151 | + |
| 152 | +## Decision |
| 153 | + |
| 154 | +**Stick with the instance-based approach** because: |
| 155 | +- FCL is a cohesive SDK, not a utility library |
| 156 | +- Developer experience is crucial for adoption |
| 157 | +- Most users will use several functions together anyway |
| 158 | +- Bundle size trade-off is acceptable for the DX benefits |
| 159 | +- Maintains clean, intuitive API surface |
| 160 | + |
| 161 | +The migration away from global state to instances was the right architectural choice. |
| 162 | + |
| 163 | +## DX vs Tree-Shaking Trade-off Summary |
| 164 | + |
| 165 | +| Aspect | Instance-Based | Context-Passing | |
| 166 | +|--------|---------------|-----------------| |
| 167 | +| **DX** | ✅ Excellent | ❌ Verbose | |
| 168 | +| **Tree-shaking** | ❌ Limited | ✅ Excellent | |
| 169 | +| **Discoverability** | ✅ Great | ❌ Poor | |
| 170 | +| **Error-prone** | ✅ Low | ❌ High | |
| 171 | +| **Bundle size** | ❌ Larger | ✅ Smaller | |
| 172 | +| **API simplicity** | ✅ Clean | ❌ Complex | |
| 173 | + |
| 174 | +**Conclusion:** For FCL, DX wins over tree-shaking optimization. |
0 commit comments