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 filter
  • stateSelector: A block that receives the full store's state and returns a new state created from the full state.
  • callback: The callback block called with the new state returned from the stateSelector

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.

750

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:

  1. We create StateSelector<TodoItemWithSettings> which is a function or block that receives the full Store state and return a TodoItemWithSettings
  2. Inside the state selector, we query values from the state using state.value(forKeyOfType: TodoList.self) to read the TodoList state and state.value(forKeyOfType: TodoSettings.self) to get the TodoSettings state.
  3. We can return nil from the state selector block. Returning nil will stop the notification and the listener block will not be called.
  4. If we return non-nil from the state selector block, this newly created instance of TodoItemWithSettings is passed to the listener notification block.
  5. The todoItemWithSettingsStateSelector is finally passed to the addListener 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