Unnecessary recomputes of Views #694
-
Hi. I stumbled upon an issue, where subviews are recomputed even their State is not changed. Basically my setup is: I have a parent view and two children view. To every children is passed scoped down store needed for them(in example just with one String). Problem is that when Timer changes state, everything is recomputed including the other child. Note. If I pass to OnTapChangeView just the value, not entire store it doesn't get recomputed(after Timer changes), however then I don't have option to send actions. Here is a demo code to show the example import SwiftUI
import ComposableArchitecture
// MARK: State
public struct DemoState: Equatable {
public var onTapChangeValue: String
public var onTimerChangeValue: String
public var counter: Int = 0
}
// MARK: Actions
public enum DemoAction: Equatable {
case onAppear
case tapToChange
case tick
}
// MARK: Environment
public struct DemoEnvironment {
let runLoop: AnySchedulerOf<RunLoop>
}
// MARK: Reducer
public let demoReducer =
Reducer<DemoState, DemoAction, DemoEnvironment>.combine(
Reducer<DemoState, DemoAction, DemoEnvironment> {
state, action, environment in
struct TimerId: Hashable {}
switch action {
case .onAppear:
state.onTapChangeValue = "tap to change"
return Effect.timer(id: TimerId(), every: 2, on: environment.runLoop)
.map { _ in .tick }
.eraseToEffect()
case .tick:
state.counter += 1
state.onTimerChangeValue = "dynamic value \(state.counter)"
return .none
case .tapToChange:
state.onTapChangeValue = "changed"
return .none
}
}
)
// MARK:- View
public struct DemoView: View {
let store: Store<DemoState, DemoAction>
public init(store: Store<DemoState, DemoAction>) {
self.store = store
}
public var body: some View {
WithViewStore(store) { viewStore in
HStack {
OnTapChangeView(store: store.scope(state: \.onTapChangeValue))
OnTimerChangeView(store: store.scope(state: \.onTimerChangeValue).actionless)
}
.onAppear {
viewStore.send(.onAppear)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
struct OnTapChangeView: View {
var store: Store<String, DemoAction>
var body: some View {
WithViewStore(store) { store in
Button.init(store.state) {
store.send(.tapToChange)
}
.background(Color.random)
}
}
}
struct OnTimerChangeView: View {
var store: Store<String, Never>
var body: some View {
WithViewStore(store) { store in
Text(store.state)
.background(Color.random)
}
}
}
public struct ContentView: View {
public var body: some View {
DemoView(store: .init(
initialState: .init(onTapChangeValue: "", onTimerChangeValue: ""),
reducer: demoReducer,
environment: .init(runLoop: .main))
)
}
}
extension ShapeStyle where Self == Color {
static var random: Color {
Color(
red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1)
)
}
} Basically, I expect the background of "tap to change" to change only when button is pressed, but it's changed all the time by the timer |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
Hi @AndreiVidrasco, this is happening because you are observing more state than necessary in your views. The exact same thing would happen in vanilla SwiftUI too if you had a big ole observable object holding the state for multiple screens. The fix is to scope the store when applying For your example in particular, the public struct DemoView: View {
let store: Store<DemoState, DemoAction>
public init(store: Store<DemoState, DemoAction>) {
self.store = store
}
public var body: some View {
- WithViewStore(store) { viewStore in
+ WithViewStore(store.stateless) { viewStore in |
Beta Was this translation helpful? Give feedback.
Hi @AndreiVidrasco, this is happening because you are observing more state than necessary in your views. The exact same thing would happen in vanilla SwiftUI too if you had a big ole observable object holding the state for multiple screens.
The fix is to scope the store when applying
ViewStore
so that you can chisel away the state to the bare essentials that the view needs to do its job. There are some examples of this in the case studies (e.g. here), and there are lots of examples of this in our isowords codebase.For your example in particular, the
DemoView
doesn't actually need any state from the view store. It just needs to send actions. So you can use the.stateless
property onStore
…