Imagine a simple scenario: HomeView
displaying a list of items with a modal AddItemView
for adding new items to that list.
Both views should have access to the same shared state:
struct ItemsState {
var items: IdentifiedArrayOf<Item>
}
Question: what is the best way to share ItemsState
with AddItemView
while ensuring the changes made to the items propagate to the shared state?
I see two options.
Option 1: define an optional AddItemState
in HomeState
which gets initialized with the shared ItemsState
:
struct HomeState {
var itemsState: ItemsState
var addItemState: AddItemState?
}
struct AddItemState {
var itemsState: ItemsState
...
}
The downside with his approach is that changes made to the itemsState
within AddItemState
will not propagate back to the itemsState
in the HomeState
. To achieve that, one has to manually override the itemsState
in the homeReducer
when the AddItemView
is dismissed, for example:
let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> { state, action, environment in
switch action {
case .setAddItemViewPresented(false):
state.itemsState = addItemState?.itemsState ?? state.itemsState
state.addItemState = nil
return .none
...
}
}
This approach can lead to synchronization bugs, in cases when HomeState
makes changes to the itemsState
while the AddItemView
is presented. Both views end up with a different source of truth, and moreover, the changes made to items in HomeView
get overridden and lost when the AddItemView
gets dismissed.
Option 2: define a computed AddItemContainerState
:
struct HomeState {
var itemsState: ItemsState
var addItemState: AddItemState
var addItemContainerState: AddItemContainerState {
get {
.init(itemsState: itemsState, addItemState: addItemState)
}
set {
self.itemsState = newValue.itemsState
self.addItemState = newValue.addItemState
}
}
}
AddItemView
would now use AddItemContainerState
instead. Thanks to the computed nature of this approach, it solves the synchronization issues we saw in option 1, but it does come with a downside.
addItemState
has to be defined as a non-optional now, because we need it in AddItemView
. This is rather inconvenient, because we always want a fresh instance of AddItemState
when the AddItemView
gets presented. It forces us to make manual invalidations of the addItemState
, right before the AddItemView
is presented, for example:
let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> { state, action, environment in
switch action {
case .setAddItemViewPresented(true):
state.addItemState = AddItemState()
state.isShowingAddItemView = true
return .none
...
}
}
While this approach is more robust when it comes to data flow, it's also inconvenient to use and less clear in my opinion.
I would love to hear your thoughts, improvement suggestions, and other options I haven't thought about. Thanks!