# How Formily's Effects System Manages Side Effects: A Deep Dive into the Heart Architecture

> Discover how Formily's effects system expertly manages side effects using its Heart architecture, lifecycle types, and effect hooks for optimized performance.

- Repository: [Alibaba/formily](https://github.com/alibaba/formily)
- Tags: deep-dive
- Published: 2026-02-24

---

**Formily's effects system isolates side effects through a dedicated subsystem centered on lifecycle types, effect hooks, and a central Heart manager that batches updates for optimal performance.**

The **Formily** form solution (maintained by Alibaba) provides a sophisticated effects system that separates side-effect logic from UI components. This system manages reactions to form initialization, field value changes, validation events, and component lifecycles through a declarative API. Understanding how Formily's effects system manages side effects requires examining three core concepts: **lifecycle types**, **effect hooks**, and the central **Heart** manager.

## Core Architecture of the Formily Effects System

Formily delegates all side-effect management to a specialized subsystem built around the `Heart` class. This architecture ensures that reactions to state changes remain predictable, type-safe, and performant.

### Lifecycle Types and the Heart Manager

At the foundation lies the **`LifeCycleTypes`** enum defined in [`packages/core/src/types.ts`](https://github.com/alibaba/formily/blob/main/packages/core/src/types.ts), which enumerates every hookable moment in a form or field's existence. These include initialization, mounting, value changes, submission, validation start/end, and unmounting events.

The **Heart** (implemented in [`packages/core/src/models/Heart.ts`](https://github.com/alibaba/formily/blob/main/packages/core/src/models/Heart.ts)) serves as the lifecycle manager. It maintains a registry of callbacks organized by `LifeCycleTypes` keys. When a form or field emits a lifecycle event, the Heart retrieves the corresponding queue of callbacks and executes them synchronously.

### The createEffectHook Factory

All effect hooks in Formily originate from the **`createEffectHook`** function located in [`packages/core/src/shared/effective.ts`](https://github.com/alibaba/formily/blob/main/packages/core/src/shared/effective.ts). This factory function registers a callback for a specific lifecycle type while guaranteeing execution inside a **`batch`** operation. Batching ensures that multiple state updates triggered by a side effect collapse into a single reactive update cycle, preventing unnecessary re-renders.

```typescript
// Conceptual implementation from effective.ts
createEffectHook(
  type: LifeCycleTypes,
  handler: (target, form) => (callback) => void
)

```

## Creating Effect Hooks for Forms and Fields

Formily distinguishes between form-level effects (global form state) and field-level effects (specific input controls), though both leverage the same underlying machinery.

### Form-Level Effect Hooks

Form-level hooks like `onFormInit`, `onFormMount`, and `onFormSubmit` are generated by the `createFormEffect` factory in [`packages/core/src/shared/onFormEffects.ts`](https://github.com/alibaba/formily/blob/main/packages/core/src/shared/onFormEffects.ts). Each hook wraps the user callback in a `batch` operation to ensure atomic execution:

```typescript
// Excerpt from onFormEffects.ts
function createFormEffect(type: LifeCycleTypes) {
  return createEffectHook(
    type,
    (form: Form) => (callback: (form: Form) => void) => {
      batch(() => {
        callback(form)  // Executes atomically within a single batch
      })
    }
  )
}

export const onFormInit = createFormEffect(LifeCycleTypes.ON_FORM_INIT)
export const onFormMount = createFormEffect(LifeCycleTypes.ON_FORM_MOUNT)

```

When `onFormMount` is called within an effects setup function, it registers the provided callback to execute when the form enters the mounted state, with all state changes batched automatically.

### Field-Level Effect Hooks with Path Patterns

Field-level hooks such as `onFieldValueChange`, `onFieldMount`, and `onFieldValidateStart` follow a similar pattern but introduce **path pattern matching**. The `createFieldEffect` factory (in [`packages/core/src/shared/onFieldEffects.ts`](https://github.com/alibaba/formily/blob/main/packages/core/src/shared/onFieldEffects.ts)) checks whether the target field's address matches the provided pattern before invoking the callback:

```typescript
// Excerpt from onFieldEffects.ts
function createFieldEffect<Result extends GeneralField = GeneralField>(type: LifeCycleTypes) {
  return createEffectHook(
    type,
    (field: Result, form: Form) =>
      (pattern: FormPathPattern, callback: (field: Result, form: Form) => void) => {
        if (FormPath.parse(pattern).matchAliasGroup(field.address, field.path)) {
          batch(() => {
            callback(field, form)
          })
        }
      }
  )
}

export const onFieldValueChange = createFieldEffect<DataField>(LifeCycleTypes.ON_FIELD_VALUE_CHANGE)

```

This pattern matching allows developers to target specific fields using glob patterns like `user.name` or `data.*.value`, ensuring effects run only on relevant field instances.

## Registering and Triggering Effects

Developers interact with the effects system primarily through the **`useFormEffects`** hook, which bridges the React or Vue component layer with the core Form model.

### Using useFormEffects

In Vue applications, the hook is implemented in [`packages/vue/src/hooks/useFormEffects.ts`](https://github.com/alibaba/formily/blob/main/packages/vue/src/hooks/useFormEffects.ts):

```typescript
export const useFormEffects = (effects?: (form: Form) => void): void => {
  const formRef = useForm()
  if (effects) formRef.value.addEffects(id, effects)
}

```

The React implementation follows an identical pattern. Developers provide a function that receives the form instance and calls various `onForm*` or `onField*` helpers:

```typescript
useFormEffects(form => {
  onFormMount(() => console.log('Form mounted'))
  onFieldValueChange('data.*', (field) => console.log('Field changed:', field.path))
})

```

### How the Heart Executes Callbacks

When `addEffects` is called (defined in [`packages/core/src/models/Form.ts`](https://github.com/alibaba/formily/blob/main/packages/core/src/models/Form.ts) at lines 460-480), it forwards the user-provided effects to the Heart:

```typescript
// Form.ts excerpt
addEffects = (id: any, effects: IFormProps['effects']) => {
  this.heart.addLifeCycles(id, runEffects(this, effects))
}

```

The `runEffects` function invokes the user callback with the form instance, allowing immediate registration of all lifecycle hooks. When a lifecycle event fires (e.g., a field value updates), the Heart iterates over stored callbacks for that specific `LifeCycleTypes` and executes them. Because each callback was wrapped in `batch` during creation, all resulting state mutations consolidate into a single update cycle.

## Reactive Effects and Automatic Cleanup

Beyond lifecycle hooks, Formily provides continuous observation capabilities through the underlying `@formily/reactive` engine.

### autorun and reaction Helpers

The **`onFormReact`** and **`onFieldReact`** helpers create persistent observers using `autorun`, while **`onFieldChange`** utilizes `reaction` to watch specific field state keys (value, error, visible, etc.):

```typescript
// onFormReact implementation concept
export function onFormReact(callback?: (form: Form) => void) {
  let dispose = null
  onFormInit(form => {
    dispose = autorun(() => {
      if (isFn(callback)) callback(form)
    })
  })
  onFormUnmount(() => {
    dispose()
  })
}

```

These reactive helpers store the disposable observer in the field's `disposers` array. When the field or form unmounts, Formily automatically invokes these disposers, preventing memory leaks and ensuring clean detachment from the reactive graph.

## Summary

- **Formily's effects system** centralizes side-effect management through the Heart class, isolating reactions from UI rendering logic.
- **`createEffectHook`** generates all effect hooks, automatically wrapping callbacks in `batch` to optimize performance.
- **Path pattern matching** in field-level hooks enables targeted reactions to specific form controls using `FormPath.parse` matching.
- **Registration** occurs through `useFormEffects`, which calls `form.addEffects` to populate the Heart's lifecycle queues.
- **Reactive helpers** like `onFormReact` leverage `autorun` for continuous observation, with automatic cleanup via stored disposers.

## Frequently Asked Questions

### What is the Heart in Formily?

The **Heart** is the central lifecycle manager located in [`packages/core/src/models/Heart.ts`](https://github.com/alibaba/formily/blob/main/packages/core/src/models/Heart.ts) that stores and executes effect callbacks. It maintains a map of lifecycle queues keyed by `LifeCycleTypes`, retrieving and executing the appropriate callbacks when form or field events fire. The Heart ensures all callbacks run synchronously and in the correct order.

### How does Formily batch state updates in effects?

Formily wraps every effect callback in the **`batch`** function from `@formily/reactive`. This ensures that any state mutations occurring within the callback (such as setting field values or modifying form state) are deferred and executed as a single atomic update. Batching prevents intermediate states from triggering redundant re-renders, significantly improving performance in complex forms.

### What is the difference between onFieldValueChange and onFieldReact?

**`onFieldValueChange`** is a lifecycle hook that fires once when a field's value changes, executing the callback inside a batch. **`onFieldReact`** creates a persistent `autorun` observer that re-executes whenever any observed value within the callback changes, not just the field's value. Use `onFieldValueChange` for one-time reactions to value mutations; use `onFieldReact` for continuous reactive computations that depend on multiple observable properties.

### How do I clean up side effects when a form unmounts?

Formily handles cleanup automatically for reactive effects by storing `autorun` and `reaction` disposables in the field's internal `disposers` array. When a field or form unmounts, these disposers are invoked automatically. For manual cleanup, use the **`onFormUnmount`** or **`onFieldUnmount`** hooks to register teardown logic, which the Heart executes when the component leaves the tree.