Skip to content

Commit cf0f2f2

Browse files
authored
Short-circuit the store cache from uncached stores (#2605)
* wip * wip * Update Sources/ComposableArchitecture/Store.swift
1 parent f19af6c commit cf0f2f2

File tree

2 files changed

+115
-3
lines changed

2 files changed

+115
-3
lines changed

Sources/ComposableArchitecture/Store.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ import SwiftUI
133133
/// of the store are also checked to make sure that work is performed on the main thread.
134134
public final class Store<State, Action> {
135135
private var bufferedActions: [Action] = []
136+
fileprivate var canCacheChildren = true
136137
fileprivate var children: [AnyHashable: AnyObject] = [:]
137138
@_spi(Internals) public var effectCancellables: [UUID: AnyCancellable] = [:]
138139
var _isInvalidated = { false }
@@ -1033,14 +1034,16 @@ extension ScopedStoreReducer: AnyScopedStoreReducer {
10331034
removeDuplicates isDuplicate: ((ChildState, ChildState) -> Bool)?
10341035
) -> Store<ChildState, ChildAction> {
10351036
let id = id?(store.stateSubject.value)
1036-
if let id = id,
1037+
if
1038+
store.canCacheChildren,
1039+
let id = id,
10371040
let childStore = store.children[id] as? Store<ChildState, ChildAction>
10381041
{
10391042
return childStore
10401043
}
10411044
let fromAction = self.fromAction as! (A) -> RootAction?
10421045
let isInvalid =
1043-
id == nil
1046+
id == nil || !store.canCacheChildren
10441047
? {
10451048
store._isInvalidated() || isInvalid?(store.stateSubject.value) == true
10461049
}
@@ -1067,6 +1070,7 @@ extension ScopedStoreReducer: AnyScopedStoreReducer {
10671070
reducer
10681071
}
10691072
childStore._isInvalidated = isInvalid
1073+
childStore.canCacheChildren = store.canCacheChildren && id != nil
10701074
childStore.parentCancellable = store.stateSubject
10711075
.dropFirst()
10721076
.sink { [weak store, weak childStore] state in
@@ -1092,7 +1096,9 @@ extension ScopedStoreReducer: AnyScopedStoreReducer {
10921096
Logger.shared.log("\(storeTypeName(of: store)).scope")
10931097
}
10941098
if let id = id {
1095-
store.children[id] = childStore
1099+
if store.canCacheChildren {
1100+
store.children[id] = childStore
1101+
}
10961102
}
10971103
return childStore
10981104
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import XCTest
2+
import ComposableArchitecture
3+
4+
@MainActor
5+
final class StoreLifetimeTests: BaseTCATestCase {
6+
func testStoreCaching() {
7+
let grandparentStore = Store(initialState: Grandparent.State()) {
8+
Grandparent()
9+
}
10+
let parentStore = grandparentStore.scope(state: \.child, action: \.child)
11+
XCTAssertTrue(parentStore === grandparentStore.scope(state: \.child, action: \.child))
12+
XCTAssertFalse(
13+
parentStore === grandparentStore.scope(state: { $0.child }, action: { .child($0) })
14+
)
15+
let childStore = parentStore.scope(state: \.child, action: \.child)
16+
XCTAssertTrue(childStore === parentStore.scope(state: \.child, action: \.child))
17+
XCTAssertFalse(
18+
childStore === parentStore.scope(state: { $0.child }, action: { .child($0) })
19+
)
20+
}
21+
22+
func testStoreInvalidation() {
23+
let grandparentStore = Store(initialState: Grandparent.State()) {
24+
Grandparent()
25+
}
26+
var parentStore: Store! = grandparentStore.scope(state: { $0.child }, action: { .child($0) })
27+
let childStore = parentStore.scope(state: \.child, action: \.child)
28+
29+
childStore.send(.tap)
30+
XCTAssertEqual(1, grandparentStore.withState(\.child.child.count))
31+
XCTAssertEqual(1, parentStore.withState(\.child.count))
32+
XCTAssertEqual(1, childStore.withState(\.count))
33+
grandparentStore.send(.incrementGrandchild)
34+
XCTAssertEqual(2, grandparentStore.withState(\.child.child.count))
35+
XCTAssertEqual(2, parentStore.withState(\.child.count))
36+
XCTAssertEqual(2, childStore.withState(\.count))
37+
38+
parentStore = nil
39+
40+
childStore.send(.tap)
41+
XCTAssertEqual(3, grandparentStore.withState(\.child.child.count))
42+
XCTAssertEqual(3, childStore.withState(\.count))
43+
grandparentStore.send(.incrementGrandchild)
44+
XCTAssertEqual(4, grandparentStore.withState(\.child.child.count))
45+
XCTAssertEqual(4, childStore.withState(\.count))
46+
}
47+
}
48+
49+
@Reducer
50+
fileprivate struct Child {
51+
struct State: Equatable {
52+
var count = 0
53+
}
54+
enum Action {
55+
case tap
56+
}
57+
var body: some ReducerOf<Self> {
58+
Reduce { state, action in
59+
switch action {
60+
case .tap:
61+
state.count += 1
62+
return .none
63+
}
64+
}
65+
}
66+
}
67+
68+
@Reducer
69+
fileprivate struct Parent {
70+
struct State: Equatable {
71+
var child = Child.State()
72+
}
73+
enum Action {
74+
case child(Child.Action)
75+
}
76+
var body: some ReducerOf<Self> {
77+
Scope(state: \.child, action: \.child) {
78+
Child()
79+
}
80+
}
81+
}
82+
83+
@Reducer
84+
fileprivate struct Grandparent {
85+
struct State: Equatable {
86+
var child = Parent.State()
87+
}
88+
enum Action {
89+
case child(Parent.Action)
90+
case incrementGrandchild
91+
}
92+
var body: some ReducerOf<Self> {
93+
Scope(state: \.child, action: \.child) {
94+
Parent()
95+
}
96+
Reduce { state, action in
97+
switch action {
98+
case .child:
99+
return .none
100+
case .incrementGrandchild:
101+
state.child.child.count += 1
102+
return .none
103+
}
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)