Ergonomic State Management: Part 2

Episode #99 • Apr 20, 2020 • Subscriber-Only

We’ve made creating and enhancing reducers more ergonomic, but we still haven’t given much attention to the ergonomics of the view layer of the Composable Architecture. This week we’ll make the Store much nicer to use by taking advantage of a new Swift feature and by enhancing it with a SwiftUI helper.

Part 2
Introduction
00:05
Dynamic member lookup
00:49
Dynamic member store
06:48
Bindings and the architecture
09:16
Binding helpers
14:48
What’s the point?
22:48

Unlock This Episode

Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.

Introduction

There’s another thing we can do to improve the ergonomics of our architecture, and that’s in the view layer. Right now, a view holds onto a view store that contains all of the state it cares about to render itself, and in order to access this state, we dive through the view store’s value property.

This episode is for subscribers only.

Subscribe to Point-Free

Access this episode, plus all past and future episodes when you become a subscriber.

See plans and pricing

Already a subscriber? Log in

Exercises

  1. Let’s add a little more polish to the Composable Architecture. Right now there is no easy way of working with optional state. In particular, it is not possible to write a reducer on non-optional state and use pullback to transform it to a reducer that works on optional state.

    Define a custom property on Reducer that transforms reducers on non-optional state to reducers on optional state.

    Solution
    extension Reducer {
      public var optional: Reducer<Value?, Action, Environment> {
        .init { value, action, environment in
          guard value != nil else { return [] }
          return self(&value!, action, environment)
        }
      }
    }
    
  2. There is also no easy way of working with collections in state. In particular, it is not possible to write a reducer on an element of state and use pullback to transform it to a reducer that works on a collection of state.

    Define an indexed method on Reducer that handles this kind of transformation such that the state’s key path is of the form WritableKeyPath<GlobalValue, [Value]>. In order to send an action to a particular element of the array, it must identify the element in some way. Take inspiration from the method’s name. 😁

    Solution

    Given some global app state:

    struct AppState {
      var list: [RowState]
    }
    

    In order to send actions to individual elements, you can identify them by index.

    enum AppAction {
      case list(index: Int, action: RowAction)
    }
    

    Which means that indexed would take a case path from AppAction to (Int, Action).

    From this we can deduce the signature and define the following method:

    extension Reducer {
      func indexed<GlobalValue, GlobalAction, GlobalEnvironment>(
        value: WritableKeyPath<GlobalValue, [Value]>,
        action: CasePath<GlobalAction, (Int, Action)>,
        environment: @escaping (GlobalEnvironment) -> Environment
      ) -> Reducer<GlobalValue, GlobalAction, GlobalEnvironment> {
        .init { globalValue, globalAction, globalEnvironment in
          guard
            let (index, localAction) = action.extract(from: globalAction)
            else { return [] }
          return self(
            &globalValue[keyPath: value][index],
            localAction,
            environment(globalEnvironment)
          )
          .map { effect in
            effect
              .map { action.embed((index, $0)) }
              .eraseToEffect()
          }
        }
      }
    }