Dependencies in reducer state

I am trying to figure out the best way to access dependencies in a reducer's state object.

I have a scenario with an associated view that has a date picker which gets its binding from an @BindableState property. The only way I can think of to get the date and calendar needed from the @Dependency properties is to have an action like onAppear that is able to set the values once the action is called.

struct SignUp: ReducerProtocol {
    struct State: Equatable {
        @BindableState var dateOfBirth = Date()
        ...
    }

    enum Action: Equatable, BindableAction { 
        case binding(BindingAction<State>)
        case onAppear
        ...
    }

	@Dependency(\.date.now) var now
	@Dependency(\.calendar) var calendar

    var body: some ReducerProtocol<State, Action> {
        BindingReducer()
        Reduce { state, action in
			switch action {
			case .binding:
				return .none
			case .onAppear:
				state.dateOfBirth = calendar.date(byAdding: .year, value: -18, to: now) ?? now
				return .none
            ...
			}
        }
    }
}

This feels like a naive solution. I could make the date optional but I will end up coalescing to a default value for the date picker. I had thought about adding an initialiser to the State object to pass in a date, but that seems to go against the idea of this dependency injection where the parent shouldn't need to pass down the dependencies and also would pass along the problem to the parent reducers state that creates this state.

2 Likes

As of version 0.46.0, and in particular this PR, this can be done pretty simply. You can define the default value of dateOfBirth in an eagerly evaluated closure, which allows you to make use of @Dependency:

@BindableState var dateOfBirth = {
  @Dependency(\.date.now) var now: Date
  @Dependency(\.calendar) var calendar: Calendar
  return calendar.date(byAdding: .year, value: -18, to: now) ?? now
}()

Or you can move that logic out to a little helper function and invoke it:

@BindableState var dateOfBirth = date18YearsAgo()

Now, this style of using @Dependency is a little more implicit. You have to look in more places in your SignUp reducer in order to track down all the dependencies actually being used.

The alternative is to explicitly pass the data to SignUp.State when it's constructed from the parent. That makes the parent explicitly depend on date and calendar, which is nice, but then you have to do the work every time you construct the state.

There are pros and cons to each style, but at least it is possible in either style, and it still plays nicely with testing and overriding dependencies (thanks to this PR).

3 Likes

Thanks for the quick reply @mbrandonw!

Funnily I initially tried to write this like your first suggestion and then couldn't override the dependency when writing tests. I think updating to the latest will be the best solution for the project.

Thanks again!

Make sure you are on 0.46.0 and try again. In order to bind dependencies at that early of a moment you have to use the initializer on TestStore that takes a trailing closure for customizing dependencies:

let store = TestStore(
  initialState: ...,
  reducer: ...
) {
  $0.date = ...
  $0.calendar = ...
}

If it doesn't work, please let us know. There may be an edge case we are missing.