Each application defines at least one reducer. In our todo app example the reducer must be able to handle the 4 actions we defined in the previous section.

1300

The reducer has two main responsibility and an optional one:

  1. Define the initial state for the todo app in the form of initialState in iOS and getEmptyState in android (check the sample below).
  2. Define the reduce function that takes both the action and the current state as parameters and returns a new state.
  3. (Optional) The stateKey for the reducer state. We expand on this parameter later.

For each action, the reducer returns a new copy of the todo state. Let's take a look at the reducer code.

struct TodoReducer: Reducer {
  var initialState = TodoList(todos: [])

  func reduce(action: Action, state: TodoList) -> TodoList? {

    if let action = action as? AddTodo {
      var newState = state
      newState.todos = newState.todos + [TodoItem(title: action.text, isCompleted: false)]
      return newState
    }

    if let action = action as? RemoveTodo {
      var newState = state
      newState.todos.remove(at: action.index)
      return newState
    }

    if let action = action as? MoveTodo {
      var newState = state
      let element = newState.todos.remove(at: action.from)
      newState.todos.insert(element, at: action.to)
      return newState
    }

    if let action = action as? ToggleTodo {
      var newState = state
      var post = newState.todos[action.index]
      post.isCompleted = !post.isCompleted
      newState.todos[action.index] = post
      return newState
    }

    return nil
  }

}
class TodoReducer : Reducer<TodoList>() {

  override fun reduce(oldState: TodoList, action: Action<*>): TodoList? {

    return when(action) {

      is AddTodo -> {
        val newTodoItem = TodoItem(title = action.text, isCompleted = false)
        oldState.copy(todos = oldState.todos + newTodoItem)
      }

      is RemoveTodo -> {
        val todoToRemove = oldState.todos[action.index]
        oldState.copy(todos = oldState.todos - todoToRemove)
      }

      is MoveTodo -> {
        val mutableTodos = oldState.todos.toMutableList()
        val itemToMove = mutableTodos.removeAt(action.from)
        mutableTodos.add(action.to, itemToMove)
        oldState.copy(todos = mutableTodos.toList())
      }

      is ToggleTodo -> {
        val mutableTodos = oldState.todos.toMutableList()
        val itemToToggle = mutableTodos.removeAt(action.index)
        val toggledItem = itemToToggle.copy(isCompleted = !itemToToggle.isCompleted)
        mutableTodos.add(action.index, toggledItem)
        oldState.copy(todos = mutableTodos.toList())
      }

      else -> null
    }
  }

  override fun getEmptyState(): TodoList = TodoList()

}
class TodoReducer extends Reducer<TodoList> {

  @Nullable
  @Override
  public TodoList reduce(@NonNull TodoList oldState, @NonNull Action<?> action) {
    switch (action.getActionType()) {

      case ADD_TODO: {
        String title = action.getData();
        TodoItem newItem = new TodoItem(title, false);
        List<TodoItem> todoItems = oldState.getTodos();
        todoItems.add(newItem);
        return new TodoList(todoItems);
      }

      case REMOVE_TODO: {
        int index = action.getData();
        List<TodoItem> todoItems = oldState.getTodos();
        todoItems.remove(index);
        return new TodoList(todoItems);
      }

      case MOVE_TODO: {
        final Pair<Integer, Integer> data = action.getData();
        int from = data.getFirst();
        int to = data.getSecond();
        List<TodoItem> todoItems = oldState.getTodos();
        TodoItem itemToMove = todoItems.remove(from);
        todoItems.add(to, itemToMove);
        return new TodoList(todoItems);
      }

      case TOGGLE_TODO: {
        int index = action.getData();
        List<TodoItem> todoItems = oldState.getTodos();
        TodoItem itemToToggle = todoItems.remove(index);
        todoItems.add(index, new TodoItem(itemToToggle.getTitle(), !itemToToggle.isCompleted()));
        return new TodoList(todoItems);
      }

    }
    
    return null;
  }

  @NonNull
  @Override
  public TodoList getEmptyState() {
    return new TodoList();
  }
}

For each of the actions, the reducer does the following:

  • Checks for the action type
  • Create a new state based on that action

Let's study how the reducer handles the AddTodo action:

if let action = action as? AddTodo {
  var newState = state
  newState.todos = newState.todos + [TodoItem(title: action.text, isCompleted: false)]
  return newState
}
is AddTodo -> {
  val newTodoItem = TodoItem(title = action.text, isCompleted = false)
  oldState.copy(todos = oldState.todos + newTodoItem)
}
case ADD_TODO: {
  String title = action.getData();
  TodoItem newItem = new TodoItem(title, false);
  List<TodoItem> todoItems = oldState.getTodos();
  todoItems.add(newItem);
  return new TodoList(todoItems);
}

The reducer first checks if the action is an AddTodo action. It then creates a new TodoItem and returns a new state that has that item appended to the list of todos.

Finally, if the reducer is unaware of this action, it returns nil/null from the reduce function. By returning nil/null, it sends a signal to the Store that it did not change the state. This, in turn, causes the store to hold off on sending a notification to the listeners.

🚧

Note on more advanced usages

If your application contains multiple screens that are not logically or functionally tied; for example, in your todo app you might have a settings screen. In this case, instead of having a single reducer with two responsibilities we can instead combine to sub reducers and have a more modular reducer structure. Head to applications with multiple states to read more.

iOS - Block Reducer

In iOS you can also create a reducer inline by using BlockReducer. For example, we can rewrite the reducer above as follows:

let todoReducer = BlockReducer(state: TodoList(todos: [])) { action, state in
  if let action = action as? AddTodo {
    var newState = state
    newState.todos = newState.todos + [TodoItem(title: action.text, isCompleted: false)]
    return newState
  }

  if let action = action as? RemoveTodo {
    var newState = state
    newState.todos.remove(at: action.index)
    return newState
  }

  // Handle other actions

  return nil
}

The BlockReducer init method takes three parameters:

  • The initialState for the reducer. This corresponds to the initialState var in the Reducer protocol.
  • The reduce block, this block will receive the action and the state and return the new state. This block corresponds to the reduce function in the Reducer protocol
  • (Optional) The stateKey for the reducer state. We expand on this parameter later.

Changing the reducer state key

If your app state consists of multiple reducers, the Store state is held as a dictionary. By default, the state key equals the state type name (class name or struct name) and value equal the sub state itself.
In some rare advanced situations, you can change the state key by passing a different when creating the reducer.

Check applications with multiple states for more info about this field. For most cases, this field will be left unset.

What's Next

We have actions, states and reducers we next cover the store that glues them together.

Related Topics

Todo app example

Other Suas architecture components
Listener

Also, check:
List of example applications built with Suas