Lens
Migration Guide
Step-by-step guide for refactoring existing code to use lenses.
Why Migrate?
Before (Spread Hell):
const updateCity = (state: State) => ({
...state,
user: {
...state.user,
address: {
...state.user.address,
city: 'NYC',
},
},
})After (Clean Lenses):
const cityLens = path<State, string>('user.address.city')
const updateCity = (state: State) => set(cityLens, 'NYC', state)Migration Steps
Step 1: Identify Repeated Update Patterns
Look for code that updates the same nested paths multiple times:
// ❌ Before: Repeated pattern
const updateUserCity = (state) => ({
...state,
user: { ...state.user, address: { ...state.user.address, city: newCity } },
})
const updateUserZip = (state) => ({
...state,
user: { ...state.user, address: { ...state.user.address, zip: newZip } },
})
// ✅ After: Define lens once, reuse everywhere
const cityLens = path<State, string>('user.address.city')
const zipLens = path<State, string>('user.address.zip')
const updateUserCity = (state, city) => set(cityLens, city, state)
const updateUserZip = (state, zip) => set(zipLens, zip, state)Step 2: Replace Simple Property Updates
// ❌ Before
const updateName = (user: User, name: string) => ({
...user,
name,
})
// ✅ After
const nameLens = prop<User, 'name'>('name')
const updateName = (user: User, name: string) => set(nameLens, name, user)Step 3: Replace Nested Updates
// ❌ Before
const updateAddress = (state: State) => ({
...state,
user: {
...state.user,
address: {
...state.user.address,
city: 'NYC',
zip: '10001',
},
},
})
// ✅ After
const cityLens = path<State, string>('user.address.city')
const zipLens = path<State, string>('user.address.zip')
const updateAddress = (state: State) =>
R.pipe(
state,
set(cityLens, 'NYC'),
set(zipLens, '10001')
)Step 4: Replace Transformations
// ❌ Before
const incrementCounter = (state: State) => ({
...state,
counter: state.counter + 1,
})
// ✅ After
const counterLens = prop<State, 'counter'>('counter')
const incrementCounter = (state: State) => over(counterLens, (n) => n + 1, state)Step 5: Replace Array Updates
// ❌ Before
const updateFirstTodo = (state: TodoState) => ({
...state,
todos: state.todos.map((todo, idx) =>
idx === 0 ? { ...todo, done: true } : todo
),
})
// ✅ After
const todosLens = prop<TodoState, 'todos'>('todos')
const updateFirstTodo = (state: TodoState) =>
over(todosLens, (todos) =>
todos.map((todo, idx) => (idx === 0 ? { ...todo, done: true } : todo))
, state)Migration Checklist
- Find all nested spread operators (3+ levels)
- Identify repeated update patterns
- Create lenses for commonly accessed paths
- Replace
setoperations withset(lens, value) - Replace transformations with
over(lens, fn) - Chain multiple updates with
pipe - Update tests to verify immutability
- Remove helper functions that manually spread
Incremental Migration
You don't have to migrate everything at once:
// ✅ Mix and match during transition
const partialMigration = (state: State) => {
// Use lenses for deep paths
const withCity = set(cityLens, 'NYC', state)
// Keep spreads for shallow updates (if preferred)
return {
...withCity,
timestamp: Date.now(),
}
}Common Pitfalls
Pitfall 1: Forgetting to Define Lenses
// ❌ Bad: Creating lenses inline repeatedly
setState((s) => set(prop<State, 'counter'>('counter'), 10, s))
setState((s) => set(prop<State, 'counter'>('counter'), 20, s))
// ✅ Good: Define once, reuse
const counterLens = prop<State, 'counter'>('counter')
setState((s) => set(counterLens, 10, s))
setState((s) => set(counterLens, 20, s))Pitfall 2: Using set When You Need over
// ❌ Bad: Reading then setting
const increment = (s) => set(counterLens, view(counterLens, s) + 1, s)
// ✅ Good: Using over
const increment = (s) => over(counterLens, (n) => n + 1, s)What's Next?
- API Reference → - Complete function signatures
- Patterns - More real-world recipes