Managing ephemeral (short-lived, UI-specific) state in Flutter can be tricky, especially when using a state management solution like Riverpod. While Riverpod is fantastic for shared business state, it explicitly warns against using providers for ephemeral state such as:
- Currently selected items
- Form state
- Animations
- Any controller (TextEditingController, AnimationController)
For example, consider storing the currently selected book in a provider:
final selectedBookProvider = StateProvider<String?>((ref) => null);
If your navigation sequence is:
/books
/books/42
/books/21
Pressing back should show /books/42. If a provider holds the selected book globally, it may still show 21, breaking expected behavior.
Using StateNotifierProvider for Page-Scoped Ephemeral State
Even though Riverpod discourages ephemeral state in providers, sometimes you want route-scoped state shared across widgets in the same page. For example:
- Multiple TextEditingController’s in a complex form
- Flags controlling UI elements or animations
- Temporary caches for a list of messages in a chat screen
Here, a StateNotifierProvider can be a reasonable choice if used correctly:
class FormPageState {
final TextEditingController nameController;
final TextEditingController emailController;
final bool isSubmitting;
FormPageState({
required this.nameController,
required this.emailController,
this.isSubmitting = false,
});
FormPageState copyWith({
TextEditingController? nameController,
TextEditingController? emailController,
bool? isSubmitting,
}) {
return FormPageState(
nameController: nameController ?? this.nameController,
emailController: emailController ?? this.emailController,
isSubmitting: isSubmitting ?? this.isSubmitting,
);
}
}
class FormPageNotifier extends StateNotifier<FormPageState> {
FormPageNotifier()
: super(FormPageState(
nameController: TextEditingController(),
emailController: TextEditingController(),
));
void setSubmitting(bool value) {
state = state.copyWith(isSubmitting: value);
}
void disposeControllers() {
state.nameController.dispose();
state.emailController.dispose();
}
}
final formPageProvider =
StateNotifierProvider.autoDispose<FormPageNotifier, FormPageState>(
(ref) => FormPageNotifier());
- autoDispose ensures the state is cleaned up when the page is popped.
- StateNotifier encapsulates all ephemeral state for the page.
- You can ref.watch(formPageProvider) in multiple widgets on the same page to reactively rebuild UI.
Best Practices
- Use autoDispose: Ensures ephemeral state doesn’t linger in memory after the page is gone.
- Avoid global providers for UI-only state: Only lift state globally if multiple pages truly need to access it.
- Use immutable state objects: For lists or maps, always create a new object when mutating to ensure proper rebuilds.
- Equatable or custom equality: For complex objects in state, implement equality to avoid unnecessary rebuilds.
- Use ref.listen() for side effects, not ref.watch(), to react to state changes without rebuilding widgets unnecessarily.
- Controllers and animations: Keep them in the state class and dispose them in dispose() method of the notifier.
About Flutter Hooks
Riverpod’s documentation often recommends flutter_hooks for managing ephemeral state — short-lived, UI-specific data such as text controllers or animations.
Hooks provide a React-style way to handle lifecycle logic without boilerplate StateFullWidget’s, using utilities like useState, useEffect, or useTextEditingController.
However, I personally decided not to use flutter_hooks in this article, and here’s why:
-
Additional learning curve
Riverpod already introduces a powerful and expressive state management model. Adding hooks on top can make the mental model more complex — especially for developers who don’t come from a React background. Concepts like useEffect or useCallback don’t always feel natural in Flutter’s declarative UI model. -
Consistency and readability
Mixing hooks and classic Riverpod notifiers in the same codebase can lead to architectural inconsistency. In a team environment, not everyone might be familiar with hooks, which can reduce code readability and maintainability. -
Riverpod already handles lifecycle effectively
The combination of StateNotifierProvider (or ChangeNotifierProivder) with autoDispose already provides robust lifecycle and cleanup management. For many cases, this is more than enough — without introducing another abstraction layer.
Flutter Hooks is a great tool — but not always a necessary one.
If your app already uses Riverpod’s built-in lifecycle management, or your team isn’t accustomed to React-style hooks, sticking with plain Riverpod is often simpler and cleaner.
Conclusion
While Riverpod discourages using providers for ephemeral state, StateNotifierProvider with autoDispose is a safe way to manage route-scoped ephemeral state, as long as:
- You don’t lift it globally unnecessarily
- You properly dispose controllers
- You respect the immutability and equality rules
This approach allows for clean, testable, and reactive UI while avoiding navigation and rebuild issues commonly associated with ephemeral state.
Thanks for reading 🎈.
