Skip to main content
Many SwiftUI APIs use bindings to set up two-way communication between your application’s state and a view. The Composable Architecture provides several tools for creating bindings that establish such communication with your store.

Ad hoc bindings

The simplest tool is to create a dedicated action that changes a piece of state.
1

Define state property

2

Define corresponding action

3

Handle the action

4

Derive binding in view

First, hold the store in a bindable way:
If targeting iOS 16 or earlier, use @Perception.Bindable instead of @Bindable.
5

Create the binding

Binding actions and reducers

For screens with many controls, creating individual actions for each binding can be tedious. The library provides BindableAction and BindingReducer to eliminate boilerplate.

The problem

Consider a settings screen with many editable fields:
Traditionally, you’d need an action for each field:
And handle each in the reducer:
This is a lot of boilerplate.

The solution

1

Conform action to BindableAction

2

Add BindingReducer

3

Make store bindable in view

4

Derive bindings with $ syntax

That’s it! All the boilerplate is eliminated.

Observing specific bindings

You can layer additional functionality over bindings by pattern matching:

Pattern matching in reducer

Using onChange

Alternatively, use onChange on the BindingReducer:

Testing bindings

Binding actions can be tested just like regular actions. Instead of sending a specific action like .displayNameChanged("Blob"), you send a BindingAction:
The first argument is a key path to the binding, and the second is the new value.

Advanced: Custom bindings

You can create custom bindings that perform additional logic:
This allows you to intercept and transform values before they reach your reducer.

Best practices

Use BindingReducer for many bindings

When you have 3+ bindable fields, use BindingReducer instead of ad hoc bindings

Validate with onChange

Use onChange to add validation or side effects for specific fields

Test binding actions

Always test binding actions to ensure proper state mutations

Keep logic in reducer

Don’t put business logic in custom binding closures; keep it in the reducer

Common patterns

Conditional bindings

Sometimes you only want to update state when certain conditions are met:

Derived bindings

You can derive bindings from computed properties:

Debounced bindings

For expensive operations, debounce binding changes:

Troubleshooting

Make sure:
  1. Your store is marked with @Bindable (or @Perception.Bindable for iOS 16)
  2. Your State is marked with @ObservableState
  3. You’re using $store syntax to derive bindings
If targeting iOS 16 or earlier, use the backported version:
Verify that:
  1. BindingReducer() is in your reducer’s body
  2. Your action enum conforms to BindableAction
  3. You have a case binding(BindingAction<State>) in your action enum