Skip to content

Commit

Permalink
Return "invalid" stores from scoping (#2601)
Browse files Browse the repository at this point in the history
* Return "invalid" stores from scoping

Scoping can sometimes be invalid, _e.g._ SwiftUI may scope through an
index of a collection that no longer exists while diffing in a
`ForEach`, which can cause a crash when the child state is eagerly
evaluated. This commit introduces the idea of an invalid store that will
only crash if the store's state is interacted with, avoiding crashes
that could happen deeper in SwiftUI.

* wip
  • Loading branch information
stephencelis authored Nov 29, 2023
1 parent efba133 commit ff27687
Showing 1 changed file with 19 additions and 4 deletions.
23 changes: 19 additions & 4 deletions Sources/ComposableArchitecture/Store.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ public final class Store<State, Action> {
private var isSending = false
var parentCancellable: AnyCancellable?
private let reducer: any Reducer<State, Action>
@_spi(Internals) public var stateSubject: CurrentValueSubject<State, Never>
@_spi(Internals) public var stateSubject: CurrentValueSubject<State, Never>!
#if DEBUG
private let mainThreadChecksEnabled: Bool
#endif
Expand Down Expand Up @@ -175,6 +175,14 @@ public final class Store<State, Action> {
}
}

fileprivate init() {
self._isInvalidated = { true }
self.reducer = EmptyReducer()
#if DEBUG
self.mainThreadChecksEnabled = true
#endif
}

deinit {
self.invalidate()
Logger.shared.log("\(storeTypeName(of: self)).deinit")
Expand Down Expand Up @@ -994,12 +1002,15 @@ private final class ScopedStoreReducer<RootState, RootAction, State, Action>: Re

@inlinable
func reduce(into state: inout State, action: Action) -> Effect<Action> {
if self.isInvalid() {
let isInvalid = self.isInvalid()
if isInvalid {
self.onInvalidate()
}
self.isSending = true
defer {
state = self.toState(self.rootStore.stateSubject.value)
if !isInvalid || state is _OptionalProtocol {
state = self.toState(self.rootStore.stateSubject.value)
}
self.isSending = false
}
if let action = self.fromAction(action),
Expand Down Expand Up @@ -1032,6 +1043,10 @@ extension ScopedStoreReducer: AnyScopedStoreReducer {
isInvalid: ((S) -> Bool)?,
removeDuplicates isDuplicate: ((ChildState, ChildState) -> Bool)?
) -> Store<ChildState, ChildAction> {
guard isInvalid.map({ $0(store.stateSubject.value) == false }) ?? true
else {
return Store()
}
let id = id?(store.stateSubject.value)
if let id = id,
let childStore = store.children[id] as? Store<ChildState, ChildAction>
Expand All @@ -1053,7 +1068,7 @@ extension ScopedStoreReducer: AnyScopedStoreReducer {
}
let reducer = ScopedStoreReducer<RootState, RootAction, ChildState, ChildAction>(
rootStore: self.rootStore,
state: { [stateSubject = store.stateSubject] _ in toChildState(stateSubject.value) },
state: { [stateSubject = store.stateSubject!] _ in toChildState(stateSubject.value) },
action: { fromChildAction($0).flatMap(fromAction) },
isInvalid: isInvalid,
onInvalidate: { [weak store] in
Expand Down

0 comments on commit ff27687

Please sign in to comment.