Lifecycle Hooks

Run actions at specific times during your form's lifecycle - like event listeners for your forms.

What Are Lifecycle Hooks?

Lifecycle hooks let you run actions at specific moments in your form's life - when it loads, when pages change, when specific fields update, and more. They're perfect for triggering API calls, tracking analytics, or reacting to user input.

The 6 Lifecycle Hooks

HookWhen It RunsUse For
onFormLoadOnce when form mountsLoad initial data, setup
onPageChangeEvery page navigationTrack page views, save progress
onContextChangeWhen watched fields changeAuto-lookup, dependent data
beforeSubmitBefore form submissionFinal validation
afterSubmitAfter successful submitShow confirmation, redirect
onInterval 🔒At regular time intervalsToken refresh, auto-save, polling

🔒 New in v0.1.1: The onInterval hook is perfect for automatic token refresh and periodic data updates.

onFormLoad - Run Once on Mount

Runs once when the form first loads. Perfect for loading initial data.

json
{
  "lifecycle": {
    "onFormLoad": [
      {
        "type": "callApi",
        "service": "loadUserProfile",
        "then": [
          {
            "type": "assignContext",
            "assign": {
              "user.name": "{{event.name}}",
              "user.email": "{{event.email}}"
            }
          }
        ]
      }
    ]
  }
}

📝 What Happens:

  1. 1. Form mounts
  2. 2. API called to load user profile
  3. 3. Response data assigned to context
  4. 4. Form displays with pre-filled data

onPageChange - Run on Navigation

Runs every time the user navigates to a different page.

json
{
  "lifecycle": {
    "onPageChange": [
      {
        "type": "callApi",
        "service": "saveProgress"
      },
      {
        "type": "callApi",
        "service": "trackPageView"
      }
    ]
  }
}

onContextChange - Run When Fields Change

Runs when specific fields change. Always use the watch array!

json
{
  "lifecycle": {
    "onContextChange": [
      {
        "watch": ["address.zipCode"],
        "actions": [
          {
            "type": "callApi",
            "service": "lookupCity",
            "then": [
              {
                "type": "assignContext",
                "assign": {
                  "address.city": "{{event.city}}",
                  "address.state": "{{event.state}}"
                }
              }
            ]
          }
        ]
      }
    ]
  }
}

⚠️ IMPORTANT: Always Use "watch"

❌ Without watch - runs on EVERY context change (SLOW!):

json
{
  "onContextChange": [
    {
      "actions": [{ "type": "callApi", "service": "expensive" }]
    }
  ]
}

✅ With watch - only runs when specific fields change (FAST!):

json
{
  "onContextChange": [
    {
      "watch": ["zipCode"],
      "actions": [{ "type": "callApi", "service": "expensive" }]
    }
  ]
}

Multiple Watched Fields

You can watch multiple fields and have multiple watch blocks:

json
{
  "lifecycle": {
    "onContextChange": [
      {
        "watch": ["address.zipCode"],
        "actions": [
          { "type": "callApi", "service": "lookupCity" }
        ]
      },
      {
        "watch": ["address.state"],
        "actions": [
          { "type": "callApi", "service": "loadCounties" }
        ]
      },
      {
        "watch": ["cart.items", "cart.discountCode"],
        "actions": [
          { "type": "callApi", "service": "recalculateTotal" }
        ]
      }
    ]
  }
}

Complete Example: Auto-Lookup Form

Let's build a form that automatically looks up city/state when you enter a ZIP code:

json
{
  "id": "autoLookupForm",
  "type": "form",
  "initialPage": "address",
  "context": {
    "address": {
      "zipCode": "",
      "city": "",
      "state": ""
    }
  },
  "lifecycle": {
    "onContextChange": [
      {
        "watch": ["address.zipCode"],
        "actions": [
          {
            "type": "callApi",
            "service": "lookupCityState",
            "then": [
              {
                "type": "assignContext",
                "assign": {
                  "address.city": "{{event.city}}",
                  "address.state": "{{event.state}}"
                }
              }
            ]
          }
        ]
      }
    ]
  },
  "services": {
    "lookupCityState": {
      "type": "rest",
      "url": "/api/lookup/{{context:address.zipCode}}",
      "method": "GET"
    }
  }
}

🎯 The Flow:

  1. 1. User types ZIP code and presses Enter (or clicks away)
  2. 2. Context updates: address.zipCode = "12345"
  3. 3. Lifecycle hook detects change (watching address.zipCode)
  4. 4. API called automatically
  5. 5. Response assigned to address.city and address.state
  6. 6. Fields auto-fill! ✨

Combining with Computed Fields

Lifecycle hooks and computed fields work great together! Use lifecycle hooks to fetch API data, then let computed fields calculate based on that data.

json
{
  "context": {
    "order": {
      "subtotal": 0,
      "tax": 0,
      "total": 0
    },
    "location": {
      "zipCode": "",
      "taxRate": 0
    }
  },
  "lifecycle": {
    "onContextChange": [
      {
        "watch": ["location.zipCode"],
        "actions": [
          {
            "type": "callApi",
            "service": "getTaxRate",
            "then": [
              {
                "type": "assignContext",
                "assign": {
                  "location.taxRate": "{{event.rate}}"
                }
              }
            ]
          }
        ]
      }
    ]
  },
  "computedFields": {
    "tax": {
      "targetPath": "order.tax",
      "expression": "context.order.subtotal * context.location.taxRate",
      "dependencies": ["order.subtotal", "location.taxRate"]
    },
    "total": {
      "targetPath": "order.total",
      "expression": "context.order.subtotal + context.order.tax",
      "dependencies": ["order.subtotal", "order.tax"]
    }
  }
}

🔄 The Magic:

  1. 1. User enters ZIP code
  2. 2. Lifecycle hook calls API → gets tax rate
  3. 3. Tax rate stored in location.taxRate
  4. 4. Computed field detects dependency change
  5. 5. Tax and total recalculate automatically!

Common Patterns

Pattern 1: Load Initial Data

json
{
  "lifecycle": {
    "onFormLoad": [
      {
        "type": "callApi",
        "service": "loadProfile",
        "then": [
          {
            "type": "assignContext",
            "assign": { "user": "{{event}}" }
          }
        ]
      }
    ]
  }
}

Pattern 2: Dependent Dropdowns

json
{
  "onContextChange": [
    {
      "watch": ["country"],
      "actions": [
        { "type": "callApi", "service": "loadStates" }
      ]
    },
    {
      "watch": ["state"],
      "actions": [
        { "type": "callApi", "service": "loadCities" }
      ]
    }
  ]
}

Pattern 3: Auto-Save

json
{
  "onContextChange": [
    {
      "watch": ["form.firstName", "form.lastName", "form.email"],
      "actions": [
        {
          "type": "callApi",
          "service": "autoSave"
        }
      ]
    }
  ]
}

Pattern 4: Analytics Tracking

json
{
  "onPageChange": [
    {
      "type": "callApi",
      "service": "trackPageView"
    }
  ]
}

Troubleshooting

❓ Hook Not Firing?

Problem: Typed in input, but lifecycle hook didn't run

Solution: Press Enter or Tab after typing! Inputs update context on blur, not every keystroke.

❓ Wrong Field Path?

Problem: Hook never triggers

json
// ❌ Wrong - forgot nested path
{ "watch": ["zipCode"] }

// ✅ Right - full path
{ "watch": ["address.zipCode"] }

❓ Hook Runs Too Often?

Problem: API called on every single change

Solution: Add watch array to only trigger on specific fields

🔒 onInterval - Periodic Actions

Runs actions at regular time intervals. Perfect for automatic token refresh, auto-save, or polling for updates.

New in v0.1.1: Use onInterval for automatic token refresh every few minutes.

Automatic Token Refresh Every 3 Minutes

json
{
  "lifecycle": {
    "onInterval": [
      {
        "interval": 180000,
        "actions": [
          {
            "type": "callApi",
            "service": "refreshToken",
            "then": [
              {
                "type": "assignSecret",
                "assign": {
                  "accessToken": "{{event:access_token}}"
                },
                "encrypt": true
              },
              {
                "type": "assignContext",
                "assign": {
                  "tokenRefreshedAt": "{{event:timestamp}}"
                }
              }
            ]
          }
        ]
      }
    ]
  }
}

Multiple Intervals

You can have multiple intervals running simultaneously:

json
{
  "lifecycle": {
    "onInterval": [
      {
        "interval": 180000,
        "actions": [
          { "type": "callApi", "service": "refreshToken" }
        ]
      },
      {
        "interval": 30000,
        "actions": [
          { "type": "callApi", "service": "checkNotifications" }
        ]
      },
      {
        "interval": 60000,
        "actions": [
          { "type": "callApi", "service": "autoSaveDraft" }
        ]
      }
    ]
  }
}

Conditional Intervals

Run interval actions only when conditions are met:

json
{
  "lifecycle": {
    "onInterval": [
      {
        "interval": 300000,
        "condition": [
          {
            "matchType": "exact",
            "field": "isAuthenticated",
            "value": true
          }
        ],
        "actions": [
          { "type": "callApi", "service": "keepSessionAlive" }
        ]
      }
    ]
  }
}

⚠️ Important Notes

  • • Intervals are automatically cleared when the form unmounts
  • • Use appropriate intervals - too frequent can impact performance
  • • Consider battery life on mobile devices
  • • Token refresh typically every 3-5 minutes is sufficient

Live Examples