Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/reatom/reatom/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The extend system is Reatom’s mechanism for adding capabilities to atoms and actions without modifying their core behavior. Extensions enable you to compose functionality in a modular, reusable way. Extensions can:
  • Add new methods to atoms/actions
  • Modify behavior with middleware
  • Transform parameters or return values
  • Add lifecycle hooks
  • Enable feature composition

Basic Usage

Extending Atoms

import { atom } from '@reatom/core'

const count = atom(0, 'count').extend(
  withReset(0),
  withLogger('COUNT')
)

// Now has additional methods from extensions
count.reset() // Added by withReset

Extension Type Signature

interface Ext<Target extends AtomLike = AtomLike, Extension = Target> {
  (target: Target): Extension
}

interface Extend<This extends AtomLike> {
  <T1>(extension1: Ext<This, T1>): Merge<This, [T1]>
  <T1, T2>(
    extension1: Ext<This, T1>,
    extension2: Ext<Merge<This, [T1]>, T2>
  ): Merge<This, [T1, T2]>
  // ... up to 10 extensions
}

Built-in Extension Utilities

withMiddleware

Add middleware that intercepts atom/action execution:
import { atom, withMiddleware } from '@reatom/core'

const withLogger = (prefix: string) =>
  withMiddleware((target) => {
    return (next, ...params) => {
      console.log(`${prefix} [${target.name}] Before:`, params)
      const result = next(...params)
      console.log(`${prefix} [${target.name}] After:`, result)
      return result
    }
  })

const counter = atom(0, 'counter').extend(
  withLogger('DEBUG')
)

counter.set(5)
// Logs: "DEBUG [counter] Before: [5]"
// Logs: "DEBUG [counter] After: 5"

withTap

Observe state changes without modifying them:
import { atom, withTap } from '@reatom/core'

const data = atom({ count: 0 }, 'data').extend(
  withTap((target, newState, prevState) => {
    console.log('Changed from', prevState, 'to', newState)
  })
)

data.set({ count: 1 })
// Logs: "Changed from {count: 0} to {count: 1}"

withParams

Transform parameters before they reach the atom:
import { atom, withParams } from '@reatom/core'

// Convert from any unit to meters
const length = atom(0, 'length').extend(
  withParams((value: number, unit: 'cm' | 'm' | 'km') => {
    switch (unit) {
      case 'cm': return value / 100
      case 'm': return value
      case 'km': return value * 1000
      default: return value
    }
  })
)

length.set(5, 'km') // Sets value to 5000 meters
length.set(100, 'cm') // Sets value to 1 meter

withComputed

Add computed capabilities to an atom:
import { atom, withComputed } from '@reatom/core'

const celsius = atom(0, 'celsius').extend(
  withComputed(c => Math.round(c))
)

celsius.set(25.7)
console.log(celsius()) // -> 26 (rounded)

Creating Custom Extensions

Extension that Returns the Target

Simplest form - modify the target and return it:
import { type Ext, type Atom } from '@reatom/core'

// Add a reset method
function withReset<T>(initialValue: T): Ext<Atom<T>> {
  return (target) => {
    return {
      reset: () => target.set(initialValue)
    }
  }
}

const counter = atom(0, 'counter').extend(withReset(0))

counter.set(10)
counter.reset() // Back to 0

Extension with Additional Methods

Add multiple methods to an atom:
import { atom, type Ext, type Atom } from '@reatom/core'

function withCounterMethods(): Ext<Atom<number>> {
  return (target) => ({
    increment: () => target.set(prev => prev + 1),
    decrement: () => target.set(prev => prev - 1),
    reset: () => target.set(0),
    add: (n: number) => target.set(prev => prev + n),
  })
}

const count = atom(0, 'count').extend(withCounterMethods())

count.increment() // 1
count.add(5) // 6
count.decrement() // 5
count.reset() // 0

Extension with Middleware

Create extensions that modify behavior:
import { withMiddleware, type Ext, type AtomLike } from '@reatom/core'

// Validate values before setting
function withValidation<T>(
  validate: (value: T) => boolean,
  errorMsg = 'Invalid value'
): Ext<Atom<T>> {
  return withMiddleware((target) => {
    return (next, value) => {
      if (!validate(value)) {
        throw new Error(errorMsg)
      }
      return next(value)
    }
  })
}

const age = atom(0, 'age').extend(
  withValidation(
    (value) => value >= 0 && value < 150,
    'Age must be between 0 and 150'
  )
)

age.set(25) // OK
age.set(-5) // Throws error
age.set(200) // Throws error

Extension with State Tracking

import { atom, withMiddleware, type Ext, type Atom } from '@reatom/core'

function withHistory<T>(): Ext<Atom<T>> {
  const history: T[] = []
  
  return (target) => {
    // Add middleware to track changes
    target.extend(
      withMiddleware(() => (next, ...params) => {
        const result = next(...params)
        history.push(result)
        return result
      })
    )
    
    // Add history methods
    return {
      getHistory: () => [...history],
      clearHistory: () => { history.length = 0 },
    }
  }
}

const value = atom(0, 'value').extend(withHistory())

value.set(1)
value.set(2)
value.set(3)

console.log(value.getHistory()) // [1, 2, 3]

Action Extensions

withActionMiddleware

Extensions specifically for actions:
import { action, withActionMiddleware } from '@reatom/core'

// Retry on failure
function withRetry(maxAttempts: number) {
  return withActionMiddleware((target) => {
    return (next, ...params) => {
      let attempts = 0
      
      while (attempts < maxAttempts) {
        try {
          return next(...params)
        } catch (error) {
          attempts++
          if (attempts >= maxAttempts) throw error
          console.log(`Retry ${attempts}/${maxAttempts}`)
        }
      }
    }
  })
}

const fetchData = action(async () => {
  const response = await fetch('/api/data')
  if (!response.ok) throw new Error('Failed')
  return response.json()
}, 'fetchData').extend(withRetry(3))

Extension Composition

Chain multiple extensions together:
import { atom } from '@reatom/core'

const counter = atom(0, 'counter').extend(
  withReset(0),
  withValidation(v => v >= 0),
  withLogger('COUNTER'),
  withHistory()
)

// Has all methods and behaviors
counter.increment()
counter.reset()
console.log(counter.getHistory())

Global Extensions

Apply extensions to all future atoms:
import { addGlobalExtension, isAction, withCallHook } from '@reatom/core'

// Track all action calls for analytics
addGlobalExtension((target) => {
  if (isAction(target)) {
    target.extend(
      withCallHook(() => {
        analytics.track(target.name)
      })
    )
  }
  return target
})

// All actions created after this will be tracked
const myAction = action(() => {...}, 'myAction')
Global extensions affect all atoms/actions in your application. Use them sparingly and only for cross-cutting concerns like logging or analytics.

Advanced Patterns

Type-Safe Extensions

import { type Ext, type Atom } from '@reatom/core'

// Extension with proper TypeScript types
interface ToggleMethods {
  toggle: () => boolean
  on: () => boolean
  off: () => boolean
}

function withToggle(): Ext<Atom<boolean>, Atom<boolean> & ToggleMethods> {
  return (target) => ({
    toggle: () => target.set(prev => !prev),
    on: () => target.set(true),
    off: () => target.set(false),
  })
}

const isOpen = atom(false, 'isOpen').extend(withToggle())

// TypeScript knows about these methods
isOpen.toggle()
isOpen.on()
isOpen.off()

Conditional Extensions

import { atom, isAtom, type Ext } from '@reatom/core'

function withDevTools<T extends AtomLike>(enabled: boolean): Ext<T> {
  return (target) => {
    if (!enabled || !isAtom(target)) return target
    
    // Only add devtools in development
    return target.extend(
      withLogger('DEVTOOLS'),
      connectToDevTools(target)
    )
  }
}

const isDev = process.env.NODE_ENV === 'development'

const data = atom(0, 'data').extend(
  withDevTools(isDev)
)

Extension Best Practices

Each extension should do one thing well:
// Good - focused extensions
const counter = atom(0).extend(
  withReset(0),
  withValidation(v => v >= 0)
)

// Avoid - doing too much in one extension
const counter = atom(0).extend(
  withEverything() // reset, validation, logging, etc.
)
Use TypeScript to ensure type safety:
// Good - typed return
function withReset<T>(init: T): Ext<Atom<T>> {
  return (target) => ({
    reset: () => target.set(init)
  })
}

// Avoid - untyped
function withReset(init: any) {
  return (target: any) => ({
    reset: () => target.set(init)
  })
}
Extensions must return the same atom or additional methods:
// Good
function myExt(): Ext {
  return (target) => {
    // Modify target or return methods
    return { newMethod: () => {} }
  }
}

// ERROR - creates new atom
function badExt(): Ext {
  return (target) => atom(0) // Don't do this!
}

Common Extension Recipes

Debounced Updates

function withDebounce<T>(ms: number): Ext<Atom<T>> {
  let timeout: any
  
  return withMiddleware(() => (next, value) => {
    clearTimeout(timeout)
    timeout = setTimeout(() => next(value), ms)
  })
}

const search = atom('', 'search').extend(
  withDebounce(300)
)

Persistence

function withLocalStorage(key: string) {
  return (target: Atom<any>) => {
    // Load from storage
    const stored = localStorage.getItem(key)
    if (stored) {
      target.set(JSON.parse(stored))
    }
    
    // Save on changes
    target.extend(
      withTap((_, state) => {
        localStorage.setItem(key, JSON.stringify(state))
      })
    )
    
    return target
  }
}

const settings = atom({}, 'settings').extend(
  withLocalStorage('app-settings')
)

Atom

Core state container

Actions

Extend actions with middleware

Computed

Extend derived state

Effects

Extend reactive effects