receta
Lens

Common Patterns & Real-World Recipes

Copy-paste-ready patterns for common scenarios.

React State Management

Pattern: Form State with Lenses

import { useState } from 'react'
import { prop, path, set, type Lens } from '@ontologyio/receta/lens'

interface FormState {
  user: {
    firstName: string
    lastName: string
    email: string
  }
  billing: {
    cardNumber: string
    expiryDate: string
  }
  agreedToTerms: boolean
}

const firstNameLens = path<FormState, string>('user.firstName')
const lastNameLens = path<FormState, string>('user.lastName')
const emailLens = path<FormState, string>('user.email')
const cardNumberLens = path<FormState, string>('billing.cardNumber')
const termsLens = prop<FormState, 'agreedToTerms'>('agreedToTerms')

export const CheckoutForm = () => {
  const [form, setForm] = useState<FormState>(initialForm)

  const updateField = <A,>(lens: Lens<FormState, A>, value: A) => {
    setForm((state) => set(lens, value, state))
  }

  return (
    <form>
      <input
        value={form.user.firstName}
        onChange={(e) => updateField(firstNameLens, e.target.value)}
      />
      <input
        value={form.user.email}
        onChange={(e) => updateField(emailLens, e.target.value)}
      />
      <input
        value={form.billing.cardNumber}
        onChange={(e) => updateField(cardNumberLens, e.target.value)}
      />
      <input
        type="checkbox"
        checked={form.agreedToTerms}
        onChange={(e) => updateField(termsLens, e.target.checked)}
      />
    </form>
  )
}

Pattern: Zustand Store

import { create } from 'zustand'
import { prop, path, set, over } from '@ontologyio/receta/lens'

interface Store {
  user: { name: string; email: string }
  settings: { theme: 'light' | 'dark'; notifications: boolean }
  counter: number
}

const themeLens = path<Store, string>('settings.theme')
const counterLens = prop<Store, 'counter'>('counter')

// Zustand's own setter is named setState so it doesn't hide the lens `set`
const useStore = create<Store & Actions>((setState) => ({
  user: { name: '', email: '' },
  settings: { theme: 'light', notifications: true },
  counter: 0,

  updateTheme: (theme) =>
    setState((state) => set(themeLens, theme, state)),

  increment: () =>
    setState((state) => over(counterLens, (n) => n + 1, state)),
}))

Redux Patterns

Pattern: Reducer with Lenses

import { prop, path, set, over } from '@ontologyio/receta/lens'
import * as R from 'remeda'

interface AppState {
  user: { isAuthenticated: boolean; profile: UserProfile }
  ui: { isLoading: boolean; modal: { isOpen: boolean } }
  data: { items: Item[] }
}

const isAuthenticatedLens = path<AppState, boolean>('user.isAuthenticated')
const isLoadingLens = path<AppState, boolean>('ui.isLoading')
const modalOpenLens = path<AppState, boolean>('ui.modal.isOpen')
const itemsLens = path<AppState, Item[]>('data.items')

const reducer = (state: AppState, action: Action): AppState => {
  switch (action.type) {
    case 'LOGIN_SUCCESS':
      return R.pipe(
        state,
        set(isAuthenticatedLens, true),
        set(isLoadingLens, false)
      )

    case 'TOGGLE_MODAL':
      return over(modalOpenLens, (open) => !open, state)

    case 'ADD_ITEM':
      return over(itemsLens, (items) => [...items, action.payload], state)

    default:
      return state
  }
}

Form Validation

Pattern: Field Errors

interface FormField<T> {
  value: T
  error?: string
  touched: boolean
}

interface Form {
  fields: {
    email: FormField<string>
    password: FormField<string>
  }
  isValid: boolean
}

const emailValueLens = path<Form, string>('fields.email.value')
const emailErrorLens = path<Form, string | undefined>('fields.email.error')
const emailTouchedLens = path<Form, boolean>('fields.email.touched')

const validateEmail = (form: Form): Form => {
  const email = view(emailValueLens, form)
  const error = email.includes('@') ? undefined : 'Invalid email'

  return R.pipe(
    form,
    set(emailErrorLens, error),
    set(emailTouchedLens, true),
    set(prop<Form, 'isValid'>('isValid'), error === undefined)
  )
}

API Response Transformation

Pattern: Normalizing Nested Data

interface APIResponse {
  data: {
    user: {
      profile: {
        displayName: string
        bio: string
        social: {
          twitter?: string
          github?: string
        }
      }
    }
  }
}

const displayNameLens = path<APIResponse, string>(
  'data.user.profile.displayName'
)
const bioLens = path<APIResponse, string>('data.user.profile.bio')
const twitterLens = path<APIResponse, string | undefined>(
  'data.user.profile.social.twitter'
)

const normalizeResponse = (response: APIResponse): APIResponse =>
  R.pipe(
    response,
    over(displayNameLens, (name) => name.trim()),
    over(bioLens, (bio) => bio.slice(0, 160)),
    over(twitterLens, (handle) =>
      handle === undefined || handle.startsWith('@') ? handle : `@${handle}`
    )
  )

Configuration Management

Pattern: Environment-Specific Config

interface Config {
  api: {
    baseUrl: string
    timeout: number
    retry: { maxAttempts: number; delay: number }
  }
  features: {
    analytics: boolean
    darkMode: boolean
  }
}

const baseUrlLens = path<Config, string>('api.baseUrl')
const timeoutLens = path<Config, number>('api.timeout')
const maxAttemptsLens = path<Config, number>('api.retry.maxAttempts')
const analyticsLens = path<Config, boolean>('features.analytics')

const toProduction = (config: Config): Config =>
  R.pipe(
    config,
    set(baseUrlLens, 'https://api.production.com'),
    set(timeoutLens, 5000),
    set(maxAttemptsLens, 5),
    set(analyticsLens, true)
  )

const toDevelopment = (config: Config): Config =>
  R.pipe(
    config,
    set(baseUrlLens, 'http://localhost:3000'),
    set(timeoutLens, 30000),
    set(analyticsLens, false)
  )

List Operations

Pattern: Update Specific Item

interface TodoList {
  todos: Array<{ id: string; text: string; done: boolean }>
}

const todosLens = prop<TodoList, 'todos'>('todos')

const toggleTodo = (state: TodoList, todoId: string): TodoList => {
  return over(todosLens, (todos) =>
    todos.map((todo) =>
      todo.id === todoId ? { ...todo, done: !todo.done } : todo
    ),
    state
  )
}

const removeTodo = (state: TodoList, todoId: string): TodoList => {
  return over(todosLens, (todos) =>
    todos.filter((todo) => todo.id !== todoId),
    state
  )
}

What's Next?

← Back to Composition

On this page