State Management

Replacing Redux with Riverpod: A Practical Migration Guide (with Code)

Migrate from Redux to Riverpod in Flutter without breaking your app. A step-by-step guide comparing Reducers, Actions, and Middleware with Notifiers and Providers.

Sachin Sharma
Sachin SharmaCreator
Jan 3, 2026
6 min read
Replacing Redux with Riverpod: A Practical Migration Guide (with Code)
Featured Resource
Quick Overview

Migrate from Redux to Riverpod in Flutter without breaking your app. A step-by-step guide comparing Reducers, Actions, and Middleware with Notifiers and Providers.

Replacing Redux with Riverpod: A Practical Migration Guide

Redux was the standard. For years, we wrote Actions, Reducers, Middleware, and huge StoreConnector widgets. We accepted the boilerplate because we believed it gave us "predictability."

But let's be honest: Redux in Flutter feels like driving a tank to the grocery store.

  • Boilerplate Hell: Adding a simple counter increment requires 4 files.
  • Stringly Typed: Until recently, actions were often string-based or required complex unions.
  • Context Dependency: You always needed a StoreProvider at the root.
  • Testing Pain: Testing layout required wrapping everything in a Store.

Enter Riverpod.

Created by Remi Rousselet (the same genius behind Provider), Riverpod is what Provider should have been. It's safe, it's compile-time checked, and it has zero boilerplate compared to Redux.

In this guide, I will show you how to take a typical Redux-based Flutter app and migrate it to Riverpod 2.0. We will cut the code size by 40% and increase readability by 100%.


The Mental Shift: From "Dispatch" to "Read/Watch"

Redux is unidirectional based on dispatching events. Riverpod is reactive based on observing state.

The Redux Way

  1. 2.
    UI Dispatches Action (dispatch(IncrementAction())).
  2. 4.
    Reducer catches Action.
  3. 6.
    Reducer returns new State.
  4. 8.
    UI rebuilds via StoreConnector.

The Riverpod Way

  1. 2.
    UI calls Method on Provider (ref.read(counterProvider.notifier).increment()).
  2. 4.
    Provider updates State.
  3. 6.
    UI rebuilds automatically because it was watching.

There are no "actions." There are just methods on a class. This simplifies the mental model drastically.


Step 1: The Store vs. The ProviderScope

In Redux, you have one global Store. In Riverpod, you have a global ProviderScope.

Redux (main.dart):

dart
final store = Store<AppState>( appReducer, initialState: AppState.initial(), middleware: [thunkMiddleware], ); void main() { runApp(StoreProvider( store: store, child: MyApp(), )); }

Riverpod (main.dart):

dart
void main() { runApp( // No store creation needed! State is lazy-loaded. ProviderScope( child: MyApp(), ), ); }

Win: We removed the initialization logic from main. State is created only when someone actually asks for it.


Step 2: Migrating State & Reducers

Redux separates the State definition from the Reducer logic. Riverpod combines them into a Notifier.

Redux (The Old Way):

dart
// 1. The State class CounterState { final int count; CounterState(this.count); } // 2. The Action class IncrementAction {} // 3. The Reducer CounterState counterReducer(CounterState state, dynamic action) { if (action is IncrementAction) { return CounterState(state.count + 1); } return state; }

Riverpod (The New Way):

dart
// 1. The Notifier (Combines State + Reducer + Actions) class CounterNotifier extends StateNotifier<int> { CounterNotifier() : super(0); // Initial state // This IS the action and the reducer combined void increment() { state = state + 1; } } // 2. The Provider (The glue) final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) { return CounterNotifier(); });

Win: We deleted the Action class. We deleted the switch-statement reducer. The logic is now a simple method.


Step 3: Migrating Thunks (Async Actions)

This is where Redux gets messy. You need redux_thunk to handle async API calls.

Redux Thunk:

dart
Function fetchUserAction = (Store<AppState> store) async { store.dispatch(LoadingAction()); try { final user = await api.getUser(); store.dispatch(UserLoadedAction(user)); } catch (e) { store.dispatch(ErrorAction(e.toString())); } };

Riverpod (AsyncNotifier): Riverpod has built-in support for Async/Await states using AsyncValue.

dart
// Define a provider that returns a Future final userProvider = FutureProvider<User>((ref) async { final api = ref.read(apiProvider); return await api.getUser(); });

Wait, that's it? Yes. Riverpod handles the loading, data, and error states for you. You don't need to manually dispatch 'LOADING' or 'ERROR' actions.

If you need a manual refresh method:

dart
class UserNotifier extends StateNotifier<AsyncValue<User>> { final Api _api; UserNotifier(this._api) : super(const AsyncValue.loading()) { fetchUser(); } Future<void> fetchUser() async { state = const AsyncValue.loading(); try { final user = await _api.getUser(); state = AsyncValue.data(user); } catch (e, st) { state = AsyncValue.error(e, st); } } }

Step 4: Connecting to the UI

Redux uses StoreConnector. Riverpod uses ConsumerWidget.

Redux:

dart
class CounterPage extends StatelessWidget { Widget build(BuildContext context) { return StoreConnector<AppState, String>( converter: (store) => store.state.count.toString(), builder: (context, count) { return Text(count); }, ); } }

Riverpod:

dart
class CounterPage extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { // 1. Watch the state final count = ref.watch(counterProvider); // 2. Use it directly return Text(count.toString()); } }

Win: No more converter. No more boilerplate builders. Just ref.watch.


Step 5: Handling Side Effects (Navigation, Snacks)

In Redux, you often need a middleware to trigger a SnackBar after an action completes. This disconnects logic from UI.

In Riverpod, we use ref.listen.

dart
class LoginPage extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { // Listen to state changes ref.listen<AuthState>(authProvider, (previous, next) { if (next.errorMessage != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(next.errorMessage!)), ); } if (next.isAuthenticated) { Navigator.of(context).pushNamed('/home'); } }); return Scaffold(/* ... */); } }

This keeps the UI logic (navigation/showing feedback) inside the UI layer, where it belongs, while keeping the business logic in the Provider.


Step 6: Testing

Testing Redux reducers is easy, but testing the integration is hard. Testing Riverpod is trivial because you can override providers.

dart
testWidgets('shows user name', (tester) async { // Override the repository to return a fake user await tester.pumpWidget( ProviderScope( overrides: [ userProvider.overrideWithValue(AsyncValue.data(User(name: 'Sachin'))), ], child: UserPage(), ), ); expect(find.text('Sachin'), findsOneWidget); });

You don't need to mock the entire store. You just mock the precise piece of state you care about.


4 Common Migration Pitfalls

  1. 2.

    Don't Migrate Everything at Once: Riverpod and Redux can coexist! Keep your StoreProvider at the root. Start migrating one feature (e.g., Settings) to Riverpod. Once comfortable, move auth, then core data.

  2. 4.

    Using ref.read inside build: NEVER do ref.read(provider) inside the build method to get state. Always use ref.watch. Use read only inside callbacks like onPressed.

  3. 6.

    Forgetting autoDispose: Redux state is usually global and permanent. Riverpod providers can be destroyed when not used (e.g., leaving a screen). Use StateNotifierProvider.autoDispose to clean up memory automatically.

  4. 8.

    Over-using Providers: Not everything needs to be global state. If a variable is only used in one widget (like isHovering), just use flutter's local useState (via flutter_hooks) or significantly setState.


Conclusion

Migrating from Redux to Riverpod is like taking off a heavy backpack. The code becomes lighter, the logic becomes clearer, and the tooling (DevTools) is superior.

You lose the "Time Travel Debugging" of Redux (which, let's be honest, you rarely used in production), but you gain type safety, composability, and speed.

If you are starting a new project in 2026, Redux is technical debt on day one. Choose Riverpod.

Resources


About the Author: Sachin Sharma is a Mobile Architect who has refactored over 10 production applications from legacy state management to modern provider patterns.

Sachin Sharma

Sachin Sharma

Software Developer & Mobile Engineer

Building digital experiences at the intersection of design and code. Sharing weekly insights on engineering, productivity, and the future of tech.