Using the StateSelector
When adding a listener to the store, we can choose to pass a stateSelector
parameter. The stateSelector
is a function that selects part of the store
's state and returns a new object that is eventually used in the notification block.
func addListener<StateType>(
if filterBlock: FilterFunction<State>? = nil,
stateSelector: @escaping StateSelector<StateType>,
callback: @escaping (StateType) -> ()) -> Subscription<StateType>
fun <E> addListener(
filter: Filter<State>,
stateSelector: StateSelector<E>,
listener: Listener<E>): Subscription
<E> Subscription addListener(
@NonNull Filter<State> filter,
@NonNull StateSelector<E> stateSelector,
@NonNull Listener<E> listener);
This version of addListener
takes 3 parameters:
filterBlock
: to choose whether the notification block should be called or not. Covered more in depth in adding a listener with a filterstateSelector
: A block that receives the fullstore
's state and returns a new state created from the full state.callback
: The callback block called with the new state returned from thestateSelector
To understand when and how to use stateSelector
let's consider an example. In our todo with settings example the application has two separate screens. The main todo screen with controls to add and view the todo items. And the Settings screen which contains options to changes the background color or the text color of the todo items.
When modeling the state for this example we can either choose to have one single state
that represents both screens. Or we can go with separate states
for each screen.
For demo porpuses, we choose different state
for each screen,
// Todo Items State
struct TodoList {
var todos: [String]
}
// Todo Settings State
struct TodoSettings {
var backgroundColor: UIColor
var textColor: UIColor
var someOtherSettings: String
var others: String
var yetMore: Bool
}
// Todo Items State
data class TodoList(val todos: List<TodoItem> = listOf())
// Todo Settings State
data class TodoSettings(
val backgroundColor: Int,
val textColor: Int,
val someOtherSettings: String,
val others: String,
val isYetMore: Boolean)
// Todo Items State
class TodoList {
private final List<TodoItem> items;
// ...
}
// Todo Settings State
class TodoSettings {
private final int backgroundColor;
private final int textColor;
private final String someOtherSettings;
private final String others;
private final boolean yetMore;
// ...
}
}
Since we added two states, we need to also define two reducers. Defining these reducers is out of the scope of this article, however, the full explanation on how to do that can be found in todo with settings example.
Since we used two separate states and two separate reducers, our store
state will be a map that contains more than one key:
{
"TodoList": TodoList(),
"TodoSettings": TodoSettings(),
"OtherStatesThatWeDontCareAbout": OtherStatesThatWeDontCareAbout(),
"Others": Others()
}
In our todo items screen, we need to use values from both the TodoList
state and the TodoSettings
state keys.

If we add a listener
to the state with type TodoList
we only get the list of todos, similarly, if we add a listener
with TodoSettings
type we only get the settings.
In this situation, we can either add a listener to the full store state or add a listener with a state converter block.
Add a listener to the full store state
In order to get values from multiple Store state keys, we add a listener that gets notified whenever any value in the store state has changed:
store.addListener { [weak self] state in
// Weak self is used here to prevent strong memory cycles
if let todoList = state.value(forKeyOfType: TodoList.self),
let settings = state.value(forKeyOfType: TodoSettings.self) {
self?.todoItemsTextView.text = todoList.todos.joined(separator: "\n")
self?.todoItemsTextView.textColor = settings.textColor
self?.todoItemsTextView.backgroundColor = settings.backgroundColor
}
}
store.addListener { state ->
val todoList = state.getState<TodoList>(TodoList::class.java)
todoListAdapter.updateList(todoList)
val todoSettings = state.getState<TodoSettings>(TodoSettings::class.java)
todoListAdapter.updateSettings(todoSettings)
}
store.addListener(state -> {
TodoList todoList = state.getState(TodoList.class);
todoListAdapter.updateList(todoList);
TodoSettings todoSettings = state.getState(TodoSettings.class);
todoListAdapter.updateSettings(todoSettings);
});
The listener we added above receives the full Store state when any values in the state changes. Inside the notification block, we query values from the state using state.value(forKeyOfType: TodoList.self)
to get the TodoList
state and state.value(forKeyOfType: TodoSettings.self)
to get the TodoSettings
state. We then used the values we read from the state to populate the UI.
It is important to note that if any of the state keys in the full state changed (including TodoList
, TodoSettings
and all the others), the listener above will be notified.
Add a listener with a state converter
We saw how we can add a listener that listens to any change in the full state. When the listener is notified we read state values from the store to populate the UI.
We can alternatively create a new struct/class
that contains all the information we need to display the todo list screen.
struct TodoItemWithSettings {
var backgroundColor: UIColor
var textColor: UIColor
var todoString: String
}
data class TodoListWithSettings(
val backgroundColor: Int,
val textColor: Int,
val todoList: TodoList)
class TodoListWithSettings {
private final int backgroundColor;
private final int textColor;
private final TodoList todoList;
// ...
}
When registering a listener, we can pass a stateSelector
block that is responsible to create a TodoItemWithSettings
from the full Store state (the dictionary we have above) . In this stateSelector
block we can read values from the Store
state and return a TodoItemWithSettings
from it.
This TodoItemWithSettings
created in the stateSelector
is then used in the notification block of the listener, which is used in the todo list screen to populate the UI.
Let's look at how we can accomplish the above:
// Create a Todo List Settings selector
let todoItemWithSettingsStateSelector: StateSelector<TodoItemWithSettings> = { state in
// state here is the full store state (the hash map)
// Read values from the state
guard
let todoList = state.value(forKeyOfType: TodoList.self),
let settings = state.value(forKeyOfType: TodoSettings.self) else {
// If we can get any of them we return nil
return nil
}
return TodoItemWithSettings(backgroundColor: settings.backgroundColor,
textColor: settings.textColor,
todoString: todoList.todos.joined(separator: "\n"))
}
// Pass the todoItemWithSettingsStateSelector to addListener
let subscription = store.addListener(stateSelector: todoItemWithSettingsStateSelector) { [weak self] state in
// Weak self is used here to prevent strong memory cycles
// State here is a `TodoItemWithSettings`
self?.todoItemsTextView.text = state.todoString
self?.todoItemsTextView.textColor = state.textColor
self?.todoItemsTextView.backgroundColor = state.backgroundColor
}
// Create a Todo List Settings selector
val todoItemWithSettingsStateSelector = { state: State ->
// State here is the full store state (the hash map)
// Read values from the state
val todoList = state.getState(TodoList::class.java)
val todoSettings = state.getState(TodoSettings::class.java)
if (todoList == null || todoSettings == null) {
null
} else {
TodoListWithSettings(
todoSettings.backgroundColor,
todoSettings.textColor,
todoList)
}
}
// Pass the todoItemWithSettingsStateSelector to addListener
store.addListener(todoItemWithSettingsStateSelector) { todoListWithSettings ->
//Let the adapter handle the settings
todoListAdapter.update(todoListWithSettings)
}
// Create a Todo List Settings selector
StateSelector<TodoListWithSettings> todoItemWithSettingsStateSelector = state -> {
// State here is the full store state (the hash map)
// Read values from the state
TodoList todoList = state.getState(TodoList.class);
TodoSettings todoSettings = state.getState(TodoSettings.class);
if (todoList == null || todoSettings == null) {
return null;
}
return new TodoListWithSettings(todoSettings.getBackgroundColor(),
todoSettings.getTextColor(),
todoList);
};
// Pass the todoItemWithSettingsStateSelector to addListener
store.addListener(todoItemWithSettingsStateSelector, todoListWithSettings -> {
//Let the adapter handle the settings
todoListAdapter.update(todoListWithSettings);
});
Let's review what we did above:
- We create
StateSelector<TodoItemWithSettings>
which is a function or block that receives the full Store state and return aTodoItemWithSettings
- Inside the state selector, we query values from the state using
state.value(forKeyOfType: TodoList.self)
to read theTodoList
state andstate.value(forKeyOfType: TodoSettings.self)
to get theTodoSettings
state. - We can return nil from the state selector block. Returning nil will stop the notification and the listener block will not be called.
- If we return non-nil from the state selector block, this newly created instance of
TodoItemWithSettings
is passed to the listener notification block. - The
todoItemWithSettingsStateSelector
is finally passed to theaddListener
function.
It is important to note that if any of the state keys in the full state changed (including TodoList
, TodoSettings
and all the others), the listener above will be notified.
iOS: Using Weak self in Listeners
If you are using self inside the listener notification, make sure to capture self weakly.
let subscription = store.addListener(stateSelector: todoItemWithSettingsStateSelector) { [weak self] state in self?.state = state }
This is important to prevent strong memory cycles that lead to leaking.
What's Next
We showed in the above example how we can split our application state to separate logical states. The Store state will be a dictionary map of all our states (Read more about applications with multiple states).
If we need access to the full state, we can either add a listener that listens to the full Store state or passing a state selector.
Related Topics
Using middlewares
Async Actions
Adding a listener with a filter
Updated about 6 years ago