Skip to content

Migrating from Provider

A side-by-side guide for migrating from the Provider package to Reacton. Provider's ChangeNotifier pattern is replaced by immutable reacton values with automatic dependency tracking.

Concept Mapping

ProviderReactonNotes
ChangeNotifierProviderReactonScope + reacton()No ChangeNotifier class needed
ChangeNotifierreacton() + computed()Replace mutable class with immutable values
Provider<T> (read-only)computed<T>()Computed derived value
FutureProvider<T>asyncReacton<T>()Async data
StreamProvider<T>asyncReacton<T>()Async data from streams
Provider.of<T>(context)context.watch(reacton)Reactive read
Provider.of<T>(context, listen: false)context.read(reacton)One-time read
Consumer<T>ReactonConsumerMulti-reacton builder
Selector<T, S>ReactonSelector or selector()Fine-grained rebuilds
MultiProviderReactonScope (single)One scope for all state
ProxyProvidercomputed()Derived from other values
notifyListeners()AutomaticPropagation is automatic on set() / update()

Side-by-Side Examples

ChangeNotifier -> reacton

Provider:

dart
class CounterModel extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }

  void decrement() {
    _count--;
    notifyListeners();
  }

  void reset() {
    _count = 0;
    notifyListeners();
  }
}

// Setup:
ChangeNotifierProvider(
  create: (_) => CounterModel(),
  child: MyApp(),
)

// Read:
final count = Provider.of<CounterModel>(context).count;
// or:
final count = context.watch<CounterModel>().count;

// Modify:
context.read<CounterModel>().increment();

Reacton:

dart
final counterReacton = reacton(0, name: 'counter');

// Setup:
ReactonScope(child: MyApp())

// Read:
final count = context.watch(counterReacton);

// Modify:
context.update(counterReacton, (c) => c + 1);  // increment
context.update(counterReacton, (c) => c - 1);  // decrement
context.set(counterReacton, 0);                 // reset

TIP

With Reacton, you never call notifyListeners(). State changes propagate automatically when you call set() or update(). There is no mutable state and no manual notification.

Complex ChangeNotifier -> Multiple Reactons

Provider:

dart
class TodoModel extends ChangeNotifier {
  List<Todo> _todos = [];
  TodoFilter _filter = TodoFilter.all;

  List<Todo> get todos => _todos;
  TodoFilter get filter => _filter;

  List<Todo> get filteredTodos {
    return switch (_filter) {
      TodoFilter.all => _todos,
      TodoFilter.active => _todos.where((t) => !t.done).toList(),
      TodoFilter.done => _todos.where((t) => t.done).toList(),
    };
  }

  int get activeCount => _todos.where((t) => !t.done).length;

  void addTodo(Todo todo) {
    _todos = [..._todos, todo];
    notifyListeners();
  }

  void toggleTodo(String id) {
    _todos = _todos.map((t) {
      if (t.id == id) return t.copyWith(done: !t.done);
      return t;
    }).toList();
    notifyListeners();
  }

  void setFilter(TodoFilter filter) {
    _filter = filter;
    notifyListeners();
  }
}

Reacton:

dart
// Separate reactons for each piece of state
final todosReacton = reacton<List<Todo>>([], name: 'todos');
final filterReacton = reacton(TodoFilter.all, name: 'filter');

// Computed reactons for derived state
final filteredTodosReacton = computed((read) {
  final todos = read(todosReacton);
  final filter = read(filterReacton);
  return switch (filter) {
    TodoFilter.all => todos,
    TodoFilter.active => todos.where((t) => !t.done).toList(),
    TodoFilter.done => todos.where((t) => t.done).toList(),
  };
}, name: 'filteredTodos');

final activeCountReacton = computed(
  (read) => read(todosReacton).where((t) => !t.done).length,
  name: 'activeCount',
);

// Usage:
// Add:
context.update(todosReacton, (todos) => [...todos, newTodo]);
// Toggle:
context.update(todosReacton, (todos) => todos.map((t) {
  if (t.id == id) return t.copyWith(done: !t.done);
  return t;
}).toList());
// Filter:
context.set(filterReacton, TodoFilter.active);

Provider.of -> context.watch / context.read

Provider:

dart
// Reactive read (rebuilds):
final counter = Provider.of<CounterModel>(context);
// or
final counter = context.watch<CounterModel>();

// One-time read (no rebuild):
final counter = Provider.of<CounterModel>(context, listen: false);
// or
final counter = context.read<CounterModel>();

Reacton:

dart
// Reactive read (rebuilds):
final count = context.watch(counterReacton);

// One-time read (no rebuild):
final count = context.read(counterReacton);

Consumer -> ReactonConsumer

Provider:

dart
Consumer<CounterModel>(
  builder: (context, counter, child) {
    return Text('${counter.count}');
  },
)

Reacton:

dart
ReactonConsumer(
  builder: (context, ref) {
    final count = ref.watch(counterReacton);
    return Text('$count');
  },
)

ReactonConsumer is more flexible than Provider's Consumer because you can watch any number of reactons in the same builder without nesting multiple Consumer widgets.

Selector -> ReactonSelector

Provider:

dart
Selector<UserModel, String>(
  selector: (_, model) => model.name,
  builder: (_, name, __) => Text(name),
)

Reacton:

dart
// Option 1: ReactonSelector widget
ReactonSelector<User, String>(
  reacton: userReacton,
  selector: (user) => user.name,
  builder: (context, name) => Text(name),
)

// Option 2: selector reacton (reusable)
final userNameReacton = selector(userReacton, (user) => user.name);
// Then in widget:
final name = context.watch(userNameReacton);

MultiProvider -> ReactonScope

Provider:

dart
MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (_) => CounterModel()),
    ChangeNotifierProvider(create: (_) => TodoModel()),
    ChangeNotifierProvider(create: (_) => ThemeModel()),
    Provider(create: (_) => ApiService()),
  ],
  child: MyApp(),
)

Reacton:

dart
// All reactons are automatically available through one scope
ReactonScope(child: MyApp())

In Reacton, there is no registration step. Top-level reacton declarations are lazily initialized when first accessed. A single ReactonScope provides access to all of them.

ProxyProvider -> computed

Provider:

dart
ProxyProvider<ApiService, UserRepository>(
  update: (_, api, __) => UserRepository(api),
)

ProxyProvider2<UserRepository, AuthService, UserBloc>(
  update: (_, userRepo, auth, __) => UserBloc(userRepo, auth),
)

Reacton:

dart
// ProxyProvider is simply a computed reacton
final userRepoReacton = computed((read) {
  final api = read(apiServiceReacton);
  return UserRepository(api);
}, name: 'userRepo');

final userBlocReacton = computed((read) {
  final userRepo = read(userRepoReacton);
  final auth = read(authServiceReacton);
  return UserBloc(userRepo, auth);
}, name: 'userBloc');

FutureProvider -> asyncReacton

Provider:

dart
FutureProvider<List<User>>(
  create: (_) => api.fetchUsers(),
  initialData: [],
)

Reacton:

dart
final usersReacton = asyncReacton<List<User>>(
  (read) => api.fetchUsers(),
  name: 'users',
);

// In widget:
final users = context.watch(usersReacton);
users.when(
  loading: () => CircularProgressIndicator(),
  data: (list) => UserList(users: list),
  error: (e, _) => Text('Error: $e'),
);

Key Differences

No Mutable State

Provider relies on mutable ChangeNotifier objects with manual notifyListeners() calls. Reacton uses immutable values with automatic propagation.

No Class Hierarchies

Provider requires extending ChangeNotifier (or StateNotifier with Riverpod). Reacton uses plain top-level declarations. Business logic lives in standalone functions or the widget layer.

Automatic Dependency Tracking

Provider's ProxyProvider requires manually specifying dependencies. Reacton's computed() automatically detects which reactons are read and tracks them as dependencies.

Fine-Grained Reactivity

With Provider, a ChangeNotifier notification rebuilds all widgets watching that notifier, regardless of which field changed. With Reacton, each piece of state is a separate reacton, so widgets only rebuild when the specific value they watch changes.

Testing Migration

Provider:

dart
test('counter increments', () {
  final model = CounterModel();
  expect(model.count, 0);
  model.increment();
  expect(model.count, 1);
});

Reacton:

dart
test('counter increments', () {
  final store = TestReactonStore();
  expect(store.get(counterReacton), 0);
  store.update(counterReacton, (c) => c + 1);
  store.expectReacton(counterReacton, 1);
});

Migration Checklist

  • [ ] Replace MultiProvider / ChangeNotifierProvider with ReactonScope
  • [ ] Replace ChangeNotifier classes with reacton() declarations
  • [ ] Replace derived getters with computed() reactons
  • [ ] Replace Provider.of<T>(context) with context.watch(reacton)
  • [ ] Replace Provider.of<T>(context, listen: false) with context.read(reacton)
  • [ ] Replace notifyListeners() calls -- they are no longer needed
  • [ ] Replace Consumer<T> with ReactonConsumer
  • [ ] Replace Selector<T, S> with ReactonSelector or selector()
  • [ ] Replace ProxyProvider with computed()
  • [ ] Replace FutureProvider with asyncReacton()
  • [ ] Update tests to use TestReactonStore instead of instantiating model classes

What's Next

Released under the MIT License.