asyncredux-connector-pattern

Implement the Connector pattern for separating smart and dumb widgets. Covers creating StoreConnector widgets, implementing VmFactory and Vm classes, building view-models, and optimizing rebuilds with view-model equality.

16 stars

Best use case

asyncredux-connector-pattern is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Implement the Connector pattern for separating smart and dumb widgets. Covers creating StoreConnector widgets, implementing VmFactory and Vm classes, building view-models, and optimizing rebuilds with view-model equality.

Teams using asyncredux-connector-pattern should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/asyncredux-connector-pattern/SKILL.md --create-dirs "https://raw.githubusercontent.com/diegosouzapw/awesome-omni-skill/main/skills/development/asyncredux-connector-pattern/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/asyncredux-connector-pattern/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How asyncredux-connector-pattern Compares

Feature / Agentasyncredux-connector-patternStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Implement the Connector pattern for separating smart and dumb widgets. Covers creating StoreConnector widgets, implementing VmFactory and Vm classes, building view-models, and optimizing rebuilds with view-model equality.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

## Overview

The connector pattern separates store access logic from UI presentation. Instead of widgets directly accessing the store via `context.state` and `context.dispatch()`, a "smart" connector widget extracts store data and passes it to a "dumb" presentational widget through constructor parameters.

## Why Use the Connector Pattern?

1. **Testing simplification** - Test UI widgets without creating a Redux store by passing mock data
2. **Separation of concerns** - UI widgets focus on appearance; connectors handle business logic
3. **Reusability** - Presentational widgets function independently of Redux
4. **Code clarity** - Widget code is not cluttered with state access and transformation logic
5. **Optimized rebuilds** - Only rebuild when the view-model changes

## The Three Components

### 1. ViewModel (Vm)

Contains only the data the UI widget requires. Extends `Vm` and lists equality fields:

```dart
class CounterViewModel extends Vm {
  final int counter;
  final String description;
  final VoidCallback onIncrement;

  CounterViewModel({
    required this.counter,
    required this.description,
    required this.onIncrement,
  }) : super(equals: [counter, description]);
}
```

The `equals` list tells AsyncRedux which fields to compare when deciding whether to rebuild. Callbacks (like `onIncrement`) should NOT be included in `equals`.

### 2. VmFactory

Transforms store state into a view-model. Extends `VmFactory` and implements `fromStore()`:

```dart
class CounterFactory extends VmFactory<AppState, CounterConnector, CounterViewModel> {
  CounterFactory(connector) : super(connector);

  @override
  CounterViewModel fromStore() => CounterViewModel(
    counter: state.counter,
    description: state.description,
    onIncrement: () => dispatch(IncrementAction()),
  );
}
```

The factory has access to:
- `state` - The store state when the factory was created
- `dispatch()` - To dispatch actions from callbacks
- `dispatchSync()` - For synchronous dispatch
- `connector` - Reference to the parent connector widget

### 3. StoreConnector

Bridges the store and UI widget:

```dart
class CounterConnector extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, CounterViewModel>(
      vm: () => CounterFactory(this),
      builder: (BuildContext context, CounterViewModel vm) => CounterWidget(
        counter: vm.counter,
        description: vm.description,
        onIncrement: vm.onIncrement,
      ),
    );
  }
}
```

The "dumb" widget receives data through constructor parameters:

```dart
class CounterWidget extends StatelessWidget {
  final int counter;
  final String description;
  final VoidCallback onIncrement;

  const CounterWidget({
    required this.counter,
    required this.description,
    required this.onIncrement,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('$counter'),
        Text(description),
        ElevatedButton(
          onPressed: onIncrement,
          child: Text('Increment'),
        ),
      ],
    );
  }
}
```

## Rebuild Optimization

Each time an action changes the store state, `StoreConnector` compares the new view-model with the previous one. It only rebuilds if they differ (based on the `equals` list).

To prevent rebuilds even when state changes, use `notify: false`:

```dart
dispatch(MyAction(), notify: false);
```

## Advanced Factory Techniques

### Accessing Connector Properties

Pass data from the connector widget to the factory:

```dart
class UserConnector extends StatelessWidget {
  final int userId;
  const UserConnector({required this.userId});

  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, UserViewModel>(
      vm: () => UserFactory(this),
      builder: (context, vm) => UserWidget(user: vm.user),
    );
  }
}

class UserFactory extends VmFactory<AppState, UserConnector, UserViewModel> {
  UserFactory(connector) : super(connector);

  @override
  UserViewModel fromStore() => UserViewModel(
    // Access connector.userId here
    user: state.users.firstWhere((u) => u.id == connector.userId),
  );
}
```

### state vs currentState()

Inside the factory:
- `state` - The state when the factory was created (final, won't change)
- `currentState()` - The current store state at the moment of the call

These usually match, but diverge in callbacks after `dispatchSync()`:

```dart
@override
UserViewModel fromStore() => UserViewModel(
  onSave: () {
    dispatchSync(SaveAction());
    // state still has old value
    // currentState() has new value after SaveAction
  },
);
```

### Using the vm Getter in Callbacks

Access already-computed view-model fields in callbacks to avoid redundant calculations:

```dart
@override
UserViewModel fromStore() => UserViewModel(
  name: state.user.name,
  onSave: () {
    // Use vm.name instead of recalculating from state
    print('Saving user: ${vm.name}');
    dispatch(SaveAction(vm.name));
  },
);
```

**Note:** The `vm` getter is only available after `fromStore()` completes. Use it in callbacks, not during view-model construction.

### Base Factory Pattern

Create a base factory to reduce boilerplate:

```dart
abstract class BaseFactory<T extends StatelessWidget, Model extends Vm>
    extends VmFactory<AppState, T, Model> {
  BaseFactory(T connector) : super(connector);

  // Common getters
  User get user => state.user;
  Settings get settings => state.settings;
}

class MyFactory extends BaseFactory<MyConnector, MyViewModel> {
  MyFactory(connector) : super(connector);

  @override
  MyViewModel fromStore() => MyViewModel(
    user: user,  // Uses inherited getter
  );
}
```

## Nullable View-Models

When you cannot generate a valid view-model (e.g., data still loading), return null:

```dart
class HomeConnector extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, HomeViewModel?>(  // Nullable type
      vm: () => HomeFactory(this),
      builder: (BuildContext context, HomeViewModel? vm) {  // Nullable param
        return (vm == null)
          ? Text("User not logged in")
          : HomePage(user: vm.user);
      },
    );
  }
}

class HomeFactory extends VmFactory<AppState, HomeConnector, HomeViewModel?> {
  HomeFactory(connector) : super(connector);

  @override
  HomeViewModel? fromStore() {  // Nullable return
    return (state.user == null)
      ? null
      : HomeViewModel(user: state.user!);
  }
}
```

## Migrating from flutter_redux

If migrating from `flutter_redux`, you can use the `converter` parameter instead of `vm`:

```dart
class MyConnector extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, ViewModel>(
      converter: (store) => ViewModel.fromStore(store),
      builder: (context, vm) => MyWidget(name: vm.name),
    );
  }
}

class ViewModel extends Vm {
  final String name;
  final VoidCallback onSave;

  ViewModel({required this.name, required this.onSave})
    : super(equals: [name]);

  static ViewModel fromStore(Store<AppState> store) {
    return ViewModel(
      name: store.state.name,
      onSave: () => store.dispatch(SaveAction()),
    );
  }
}
```

Note: `vm` and `converter` are mutually exclusive. The `vm` approach is recommended for new code.

## Debugging Rebuilds

To observe when connectors rebuild, pass a `modelObserver` to the store:

```dart
var store = Store<AppState>(
  initialState: AppState.initialState(),
  modelObserver: DefaultModelObserver(),
);
```

Add `debug: this` to StoreConnector for connector type names in logs:

```dart
StoreConnector<AppState, ViewModel>(
  debug: this,
  vm: () => Factory(this),
  builder: (context, vm) => MyWidget(vm: vm),
);
```

Override `toString()` in your ViewModel for custom diagnostic output:

```dart
class MyViewModel extends Vm {
  final int counter;
  MyViewModel({required this.counter}) : super(equals: [counter]);

  @override
  String toString() => 'MyViewModel{counter: $counter}';
}
```

Console output shows rebuild information:
```
Model D:1 R:1 = Rebuild:true, Connector:MyWidgetConnector, Model:MyViewModel{counter: 5}
```

## Testing View-Models

Use `Vm.createFrom()` to test view-models in isolation:

```dart
test('view-model properties', () {
  var store = Store<AppState>(initialState: AppState(name: "Mary"));
  var vm = Vm.createFrom(store, MyFactory());

  expect(vm.name, "Mary");
});

test('view-model callbacks dispatch actions', () async {
  var store = Store<AppState>(initialState: AppState(name: "Mary"));
  var vm = Vm.createFrom(store, MyFactory());

  vm.onChangeName("Bill");
  await store.waitActionType(ChangeNameAction);
  expect(store.state.name, "Bill");
});
```

**Important:** `Vm.createFrom()` can only be called once per factory instance. Create a new factory for each test.

## Complete Example

```dart
// State
class AppState {
  final int counter;
  final String description;
  AppState({required this.counter, required this.description});
  AppState copy({int? counter, String? description}) => AppState(
    counter: counter ?? this.counter,
    description: description ?? this.description,
  );
}

// Action
class IncrementAction extends ReduxAction<AppState> {
  @override
  AppState reduce() => state.copy(counter: state.counter + 1);
}

// View-Model
class CounterViewModel extends Vm {
  final int counter;
  final String description;
  final VoidCallback onIncrement;

  CounterViewModel({
    required this.counter,
    required this.description,
    required this.onIncrement,
  }) : super(equals: [counter, description]);
}

// Factory
class CounterFactory extends VmFactory<AppState, CounterConnector, CounterViewModel> {
  CounterFactory(connector) : super(connector);

  @override
  CounterViewModel fromStore() => CounterViewModel(
    counter: state.counter,
    description: state.description,
    onIncrement: () => dispatch(IncrementAction()),
  );
}

// Connector (Smart Widget)
class CounterConnector extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, CounterViewModel>(
      vm: () => CounterFactory(this),
      builder: (context, vm) => CounterWidget(
        counter: vm.counter,
        description: vm.description,
        onIncrement: vm.onIncrement,
      ),
    );
  }
}

// Presentational Widget (Dumb Widget)
class CounterWidget extends StatelessWidget {
  final int counter;
  final String description;
  final VoidCallback onIncrement;

  const CounterWidget({
    required this.counter,
    required this.description,
    required this.onIncrement,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('$counter', style: TextStyle(fontSize: 48)),
        Text(description),
        SizedBox(height: 20),
        ElevatedButton(
          onPressed: onIncrement,
          child: Text('Increment'),
        ),
      ],
    );
  }
}
```

## References

URLs from the documentation:
- https://asyncredux.com/sitemap.xml
- https://asyncredux.com/flutter/connector/connector-pattern
- https://asyncredux.com/flutter/connector/store-connector
- https://asyncredux.com/flutter/connector/advanced-view-model
- https://asyncredux.com/flutter/connector/cannot-generate-view-model
- https://asyncredux.com/flutter/connector/migrating-from-flutter-redux
- https://asyncredux.com/flutter/testing/testing-the-view-model
- https://asyncredux.com/flutter/basics/using-the-store-state
- https://asyncredux.com/flutter/miscellaneous/observing-rebuilds

Related Skills

atlan-sql-connector-patterns

16
from diegosouzapw/awesome-omni-skill

Select and apply the correct SQL connector implementation pattern (SDK-default minimal or source-specific custom). Use when building or extending SQL metadata/query extraction connectors.

asyncio-concurrency-patterns

16
from diegosouzapw/awesome-omni-skill

Complete guide for asyncio concurrency patterns including event loops, coroutines, tasks, futures, async context managers, and performance optimization

async-python-patterns

16
from diegosouzapw/awesome-omni-skill

Master Python asyncio, concurrent programming, and async/await patterns for high-performance applications. Use when building async APIs, concurrent systems, or I/O-bound applications requiring non-blocking operations.

async-patterns-guide

16
from diegosouzapw/awesome-omni-skill

Guides users on modern async patterns including native async fn in traits, async closures, and avoiding async-trait when possible. Activates when users work with async code.

async-await-patterns

16
from diegosouzapw/awesome-omni-skill

Use when writing JavaScript or TypeScript code with asynchronous operations

astro-patterns

16
from diegosouzapw/awesome-omni-skill

Astro best practices, routing patterns, component architecture, and static site generation techniques. Use when building Astro websites, setting up routing, designing component architecture, configuring static site generation, optimizing build performance, implementing content strategies, or when user mentions Astro patterns, routing, component design, SSG, static sites, or Astro best practices.

argparse-patterns

16
from diegosouzapw/awesome-omni-skill

Standard library Python argparse examples with subparsers, choices, actions, and nested command patterns. Use when building Python CLIs without external dependencies, implementing argument parsing, creating subcommands, or when user mentions argparse, standard library CLI, subparsers, argument validation, or nested commands.

architecture-patterns

16
from diegosouzapw/awesome-omni-skill

Software architecture patterns and best practices

architecture-pattern-selector

16
from diegosouzapw/awesome-omni-skill

Recommend architecture patterns (monolith, microservices, serverless, modular monolith) based on scale, team size, and constraints. Use when cto-architect needs to select the right architectural approach for a new system or migration.

architectural-patterns-large-react

16
from diegosouzapw/awesome-omni-skill

Establish scalable architecture using modular boundaries, domain-driven design, and consistent data access patterns.

architectural-pattern-discovery

16
from diegosouzapw/awesome-omni-skill

Discovers architectural and design patterns across all abstraction levels. Analyzes structural patterns, component relationships, recurring solution approaches, and design principles. Works with any technology stack without prior framework knowledge to provide comprehensive pattern understanding from code-level to system-level architecture.

arch_patterns

16
from diegosouzapw/awesome-omni-skill

Architecture patterns - monolith vs microservices, layered, event-driven, CQRS.