.claude/skills/asyncredux-selectors/SKILL.md
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.
npx skillsauth add marcglasberg/async_redux asyncredux-selectorsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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:
data-ai
Show loading states and handle action failures in widgets. Covers `isWaiting(ActionType)` for spinners, `isFailed(ActionType)` for error states, `exceptionFor(ActionType)` for error messages, and `clearExceptionFor()` to reset failure states.
data-ai
Use `waitCondition()` inside actions to pause execution until state meets criteria. Covers waiting for price thresholds, coordinating between actions, and implementing conditional workflows.
testing
Handle user-facing errors with UserException. Covers throwing UserException from actions, setting up UserExceptionDialog, customizing error dialogs with `onShowUserExceptionDialog`, and using UserExceptionAction for non-interrupting error display.
tools
Implement undo/redo functionality using state observers. Covers recording state history with stateObserver, creating a RecoverStateAction, implementing undo for the full state or partial state, and managing history limits.