Claude Code for Immer: Immutable State with Mutable Syntax — Claude Skills 360 Blog
Blog / Frontend / Claude Code for Immer: Immutable State with Mutable Syntax
Frontend

Claude Code for Immer: Immutable State with Mutable Syntax

Published: March 19, 2027
Read time: 7 min read
By: Claude Skills 360

Immer makes immutable updates feel like plain mutations — produce(state, draft => { draft.user.name = "Alice" }) returns a new immutable object with structural sharing. Under the hood Immer uses Proxies to record mutations on the draft, then creates a minimally changed copy. useImmer is the React hook equivalent of useState with a draft updater. useImmerReducer replaces useReducer with draft-based reducers. enablePatches() activates the patches plugin — produceWithPatches returns [nextState, patches, inversePatches] for undo/redo. createDraft + finishDraft gives manual lifecycle control. Redux Toolkit’s createSlice uses Immer internally — slice reducers can mutate state directly. Curried producers const addItem = produce((draft, item) => { draft.items.push(item) }) create reusable updaters. Claude Code generates Immer produce recipes, useImmer hooks, patch-based undo/redo, and Redux Toolkit slices with Immer mutations for complex nested state.

CLAUDE.md for Immer

## Immer Stack
- Version: immer >= 10.0
- Produce: const next = produce(state, draft => { draft.x = 1 }) — immutable update
- Hook: const [state, updateState] = useImmer(initial) — setState with draft
- Reducer: const [state, dispatch] = useImmerReducer(reducer, initial) — draft reducer
- Patches: enablePatches() once at app init — produceWithPatches for undo/redo
- Redux: RTK createSlice reducers already use Immer — mutate state directly
- Curried: const updater = produce((draft, arg) => {...}) — reusable recipe
- Freeze: immer.setAutoFreeze(true) — default in dev, disable for perf in prod if needed

Core Produce Patterns

// lib/state-utils.ts — produce recipes
import { produce, enablePatches, produceWithPatches, applyPatches, type Draft } from "immer"

enablePatches()  // Call once at app startup

interface CartItem {
  productId: string
  name: string
  priceCents: number
  quantity: number
}

interface CartState {
  items: CartItem[]
  couponCode: string | null
  notes: string
}

// Simple nested update — traditional approach requires spreading every level
function addToCartManual(state: CartState, item: CartItem): CartState {
  const existing = state.items.find(i => i.productId === item.productId)
  if (existing) {
    return {
      ...state,
      items: state.items.map(i =>
        i.productId === item.productId ? { ...i, quantity: i.quantity + 1 } : i
      ),
    }
  }
  return { ...state, items: [...state.items, item] }
}

// Same thing with Immer — reads like mutation, produces immutable result
const addToCartImmer = produce((draft: Draft<CartState>, item: CartItem) => {
  const existing = draft.items.find(i => i.productId === item.productId)
  if (existing) {
    existing.quantity += 1
  } else {
    draft.items.push(item)
  }
})

const removeFromCart = produce((draft: Draft<CartState>, productId: string) => {
  const index = draft.items.findIndex(i => i.productId === productId)
  if (index !== -1) draft.items.splice(index, 1)
})

const updateQuantity = produce(
  (draft: Draft<CartState>, productId: string, quantity: number) => {
    const item = draft.items.find(i => i.productId === productId)
    if (item) {
      if (quantity <= 0) {
        draft.items.splice(draft.items.indexOf(item), 1)
      } else {
        item.quantity = quantity
      }
    }
  }
)

const applyCoupon = produce((draft: Draft<CartState>, code: string) => {
  draft.couponCode = code
})

const clearCart = produce((draft: Draft<CartState>) => {
  draft.items = []
  draft.couponCode = null
  draft.notes = ""
})

export const cartUpdaters = {
  addToCart: addToCartImmer,
  removeFromCart,
  updateQuantity,
  applyCoupon,
  clearCart,
}

useImmer React Hook

// components/CartManager.tsx — useImmer for component state
"use client"
import { useImmer } from "use-immer"

interface OrderFormState {
  items: { productId: string; name: string; quantity: number; priceCents: number }[]
  shipping: {
    firstName: string
    lastName: string
    address: string
    city: string
    state: string
    zip: string
    country: string
  }
  billing: {
    sameAsShipping: boolean
    address?: string
    city?: string
    zip?: string
  }
  notes: string
  giftMessage: string | null
}

export function OrderForm() {
  const [form, updateForm] = useImmer<OrderFormState>({
    items: [],
    shipping: { firstName: "", lastName: "", address: "", city: "", state: "", zip: "", country: "US" },
    billing: { sameAsShipping: true },
    notes: "",
    giftMessage: null,
  })

  // Deep nested update — no spreading needed
  function updateShippingField(field: keyof OrderFormState["shipping"], value: string) {
    updateForm(draft => {
      draft.shipping[field] = value
    })
  }

  function toggleBillingSameAsShipping() {
    updateForm(draft => {
      draft.billing.sameAsShipping = !draft.billing.sameAsShipping
      if (draft.billing.sameAsShipping) {
        // Clear separate billing when toggling back
        delete draft.billing.address
        delete draft.billing.city
        delete draft.billing.zip
      }
    })
  }

  function addItem(item: OrderFormState["items"][0]) {
    updateForm(draft => {
      const existing = draft.items.find(i => i.productId === item.productId)
      if (existing) {
        existing.quantity++
      } else {
        draft.items.push(item)
      }
    })
  }

  function removeItem(productId: string) {
    updateForm(draft => {
      draft.items = draft.items.filter(i => i.productId !== productId)
    })
  }

  const total = form.items.reduce((sum, i) => sum + i.priceCents * i.quantity, 0)

  return (
    <div className="space-y-6">
      <div className="grid grid-cols-2 gap-4">
        <input
          value={form.shipping.firstName}
          onChange={e => updateShippingField("firstName", e.target.value)}
          placeholder="First name"
          className="border rounded px-3 py-2"
        />
        <input
          value={form.shipping.lastName}
          onChange={e => updateShippingField("lastName", e.target.value)}
          placeholder="Last name"
          className="border rounded px-3 py-2"
        />
      </div>
      <label className="flex items-center gap-2 text-sm">
        <input
          type="checkbox"
          checked={form.billing.sameAsShipping}
          onChange={toggleBillingSameAsShipping}
        />
        Billing same as shipping
      </label>
      <div className="text-sm font-medium">
        Total: ${(total / 100).toFixed(2)} ({form.items.length} items)
      </div>
    </div>
  )
}

Patch-Based Undo/Redo

// lib/undo-redo.ts — patches for history
import { enablePatches, produceWithPatches, applyPatches, type Patch } from "immer"

enablePatches()

interface HistoryEntry {
  patches: Patch[]
  inversePatches: Patch[]
}

export function createUndoableState<S>(initialState: S) {
  let current = initialState
  const history: HistoryEntry[] = []
  let cursor = -1  // Points to last applied patch

  function apply(recipe: (draft: S) => void): S {
    const [next, patches, inversePatches] = produceWithPatches(current, recipe)

    if (patches.length === 0) return current  // No change

    // Discard future if we're mid-history
    history.splice(cursor + 1)
    history.push({ patches, inversePatches })
    cursor = history.length - 1
    current = next
    return current
  }

  function undo(): S | null {
    if (cursor < 0) return null
    const { inversePatches } = history[cursor]!
    current = applyPatches(current, inversePatches)
    cursor--
    return current
  }

  function redo(): S | null {
    if (cursor >= history.length - 1) return null
    cursor++
    const { patches } = history[cursor]!
    current = applyPatches(current, patches)
    return current
  }

  return {
    get state() { return current },
    apply,
    undo,
    redo,
    canUndo: () => cursor >= 0,
    canRedo: () => cursor < history.length - 1,
  }
}

// Usage in a React hook
import { useState, useCallback } from "react"

export function useUndoableImmer<S>(initialState: S) {
  const [, forceUpdate] = useState(0)
  const manager = useState(() => createUndoableState(initialState))[0]

  const update = useCallback((recipe: (draft: S) => void) => {
    manager.apply(recipe)
    forceUpdate(n => n + 1)
  }, [manager])

  const undo = useCallback(() => {
    manager.undo()
    forceUpdate(n => n + 1)
  }, [manager])

  const redo = useCallback(() => {
    manager.redo()
    forceUpdate(n => n + 1)
  }, [manager])

  return {
    state: manager.state,
    update,
    undo,
    redo,
    canUndo: manager.canUndo(),
    canRedo: manager.canRedo(),
  }
}

useImmerReducer

// reducers/order-reducer.ts — useImmerReducer
import { useImmerReducer } from "use-immer"
import type { Draft } from "immer"

interface OrderState {
  status: "draft" | "pending" | "confirmed" | "shipped" | "delivered"
  items: { id: string; name: string; quantity: number; priceCents: number }[]
  discountCents: number
  notes: string
}

type OrderAction =
  | { type: "ADD_ITEM"; item: OrderState["items"][0] }
  | { type: "REMOVE_ITEM"; id: string }
  | { type: "SET_QUANTITY"; id: string; quantity: number }
  | { type: "APPLY_DISCOUNT"; cents: number }
  | { type: "UPDATE_STATUS"; status: OrderState["status"] }
  | { type: "ADD_NOTE"; note: string }

function orderReducer(draft: Draft<OrderState>, action: OrderAction) {
  switch (action.type) {
    case "ADD_ITEM": {
      const existing = draft.items.find(i => i.id === action.item.id)
      if (existing) {
        existing.quantity += action.item.quantity
      } else {
        draft.items.push(action.item)
      }
      break
    }
    case "REMOVE_ITEM":
      draft.items = draft.items.filter(i => i.id !== action.id)
      break
    case "SET_QUANTITY": {
      const item = draft.items.find(i => i.id === action.id)
      if (item) item.quantity = Math.max(0, action.quantity)
      draft.items = draft.items.filter(i => i.quantity > 0)
      break
    }
    case "APPLY_DISCOUNT":
      draft.discountCents = action.cents
      break
    case "UPDATE_STATUS":
      draft.status = action.status
      break
    case "ADD_NOTE":
      draft.notes = action.note
      break
  }
}

const initialOrderState: OrderState = {
  status: "draft",
  items: [],
  discountCents: 0,
  notes: "",
}

export function useOrderReducer() {
  return useImmerReducer(orderReducer, initialOrderState)
}

For the Zustand alternative when global cross-component state is needed rather than component or reducer-local state — Zustand also supports Immer middleware (immer middleware) for mutable store updates while adding singleton store access patterns outside React, see the Zustand Advanced guide. For the structuredClone alternative for simple single-level updates where Immer’s proxy overhead is overkill — structuredClone deep-clones a value natively in modern runtimes without any library but produces a full copy with no structural sharing, see the performance guide for when to prefer native cloning. The Claude Skills 360 bundle includes Immer skill sets covering produce recipes, useImmer hooks, and patch-based undo/redo. Start with the free tier to try immutable state generation.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free