Create and cache selectors for efficient state access. Covers writing selector functions, caching with cache1 and cache2, the reselect pattern, and avoiding repeated computations in widgets.
Selectors are functions that extract specific data from the Redux store state. They provide three key benefits:
When displaying filtered or computed data, without selectors you might write:
// INEFFICIENT - filters the entire list on every access
state.users.where((user) => user.name.startsWith("A")).toList()[index].name;
This filtering operation runs every time the widget rebuilds, even when the data hasn't changed.
Create a selector function that performs the computation once:
List<User> selectUsersStartingWith(AppState state, String text) {
return state.users.where((user) => user.name.startsWith(text)).toList();
}
For expensive computations, wrap your selector with a cache function. AsyncRedux provides built-in caching utilities.
List<User> selectUsersStartingWith(AppState state, {required String text}) =>
_selectUsersStartingWith(state)(text);
static final _selectUsersStartingWith = cache1state_1param(
(AppState state) => (String text) =>
state.users.where((user) => user.name.startsWith(text)).toList()
);
For better performance, only depend on the specific state subset that matters:
List<User> selectUsersStartingWith(AppState state, {required String text}) =>
_selectUsersStartingWith(state.users)(text);
static final _selectUsersStartingWith = cache1state_1param(
(List<User> users) => (String text) =>
users.where((user) => user.name.startsWith(text)).toList()
);
This version only recalculates when state.users changes, not when any part of state changes.
AsyncRedux provides these caching functions:
| Function | States | Parameters | Use Case |
|---|---|---|---|
cache1state |
1 | 0 | Simple computed value from one state |
cache1state_1param |
1 | 1 | Filtered/computed value with one param |
cache1state_2params |
1 | 2 | Computation with two parameters |
cache1state_0params_x |
1 | Many | Variable number of parameters |
cache2states |
2 | 0 | Combines two state portions |
cache2states_1param |
2 | 1 | Combines two states with one param |
cache2states_2params |
2 | 2 | Combines two states with two params |
cache2states_0params_x |
2 | Many | Two states, variable params |
cache3states |
3 | 0 | Combines three state portions |
cache3states_0params_x |
3 | Many | Three states, variable params |
The naming convention: cache[N]state[s]_[M]param[s] where N = number of states, M = number of parameters.
Create selectors that actions can use via a dedicated class:
class ActionSelect {
final AppState state;
ActionSelect(this.state);
List<Item> get items => state.items;
Item get selectedItem => state.selectedItem;
Item? findById(int id) =>
state.items.firstWhereOrNull((item) => item.id == id);
Item? searchByText(String text) =>
state.items.firstWhereOrNull((item) => item.text.contains(text));
int get selectedIndex => state.items.indexOf(state.selectedItem);
}
Add a getter in your base action:
abstract class AppAction extends ReduxAction<AppState> {
ActionSelect get select => ActionSelect(state);
}
Usage in actions:
class LoadItemAction extends AppAction {
final int itemId;
LoadItemAction(this.itemId);
@override
AppState? reduce() {
var item = select.findById(itemId);
if (item == null) return null;
return state.copy(selectedItem: item);
}
}
Create a WidgetSelect class for organized widget-level selectors:
class WidgetSelect {
final BuildContext context;
WidgetSelect(this.context);
List<Item> get items => context.select((st) => st.items);
Item get selectedItem => context.select((st) => st.selectedItem);
Item? findById(int id) =>
context.select((st) => st.items.firstWhereOrNull((item) => item.id == id));
Item? searchByText(String text) =>
context.select((st) => st.items.firstWhereOrNull((item) => item.text.contains(text)));
int get selectedIndex =>
context.select((st) => st.items.indexOf(st.selectedItem));
}
Add to your BuildContext extension:
extension BuildContextExtension on BuildContext {
AppState get state => getState<AppState>();
R select<R>(R Function(AppState state) selector) => getSelect<AppState, R>(selector);
WidgetSelect get selector => WidgetSelect(this);
}
Usage in widgets:
Widget build(BuildContext context) {
final item = context.selector.findById(42);
return Text(item?.name ?? 'Not found');
}
Widget selectors can leverage action selectors to avoid duplication:
class WidgetSelect {
final BuildContext context;
WidgetSelect(this.context);
Item? findById(int id) =>
context.select((st) => ActionSelect(st).findById(id));
Item? searchByText(String text) =>
context.select((st) => ActionSelect(st).searchByText(text));
}
Never use context.state inside selector functions - this defeats selective rebuilding:
// WRONG - rebuilds on any state change
var items = context.select((state) => context.state.items.where(...));
// CORRECT - only rebuilds when items change
var items = context.select((state) => state.items.where(...));
Nesting context.select causes errors:
// WRONG - will cause errors
var result = context.select((state) =>
context.select((s) => s.items).where(...) // Nested select!
);
// CORRECT
var items = context.select((state) => state.items);
var result = items.where(...).toList();
AsyncRedux's built-in caching differs from the external reselect package:
| Feature | AsyncRedux | reselect |
|---|---|---|
| Results per selector | Multiple (different params) | One only |
| Memory on state change | Discards old cache | Retains indefinitely |
// Selector with caching
class UserSelectors {
static List<User> usersStartingWith(AppState state, String prefix) =>
_usersStartingWith(state.users)(prefix);
static final _usersStartingWith = cache1state_1param(
(List<User> users) => (String prefix) =>
users.where((u) => u.name.startsWith(prefix)).toList()
);
static List<User> activeUsers(AppState state) =>
_activeUsers(state.users);
static final _activeUsers = cache1state(
(List<User> users) => users.where((u) => u.isActive).toList()
);
}
// Usage in widget
Widget build(BuildContext context) {
var filtered = context.select(
(state) => UserSelectors.usersStartingWith(state, 'A')
);
return ListView.builder(
itemCount: filtered.length,
itemBuilder: (_, i) => Text(filtered[i].name),
);
}
URLs from the documentation: