Skip to content

[Bug]: HookScope invalidates view on each Environment change #29

Open
@josefdolezal

Description

@josefdolezal

Checklist

  • This is not a bug caused by platform.
  • Reviewed the README and documentation.
  • Checked existing issues & PRs to ensure not duplicated.

What happened?

Consider a simple SwiftUI app:

@main
struct _App: App {
    var body: some Scene {
        WindowGroup {
            __App__
        }
    }
}

where __App__ is a placeholder for either plain SwiftUI App or its Hooks alternative.

SwiftUI App
struct SwiftUIApp: View {
    var body: some View {
        let _ = print("SwiftUI - I changed")
        Text("")
    }
}
Hooks App
func HooksApp() -> some View {
    HookScope {
        let _ = print("HookScope - I changed")
        Text("")
    }
}

When you run both apps from Xcode 13.4.1 on iOS 15.5, you will see the following output in the console:

SwiftUIApp() HooksApp()
SwiftUI - I changed
 
 
HookScope - I changed
HookScope - I changed
HookScope - I changed

Considering both apps are stateless and do not invalidate its body, the console result should match.
When tracking down the issue using SwiftUI's _printChange(), it's clear that it's caused by library's HookScopeBody struct:

private struct HookScopeBody<Content: View>: View {
    @Environment(\.self) private var environment
    ...
    var body: some View {
        if #available(iOS 15.0, *) {
+       let _ = Self._printChanges()
        dispatcher.scoped(environment: environment, content)
    }
}

Changes printed by SwiftUI are:

HookScopeBody: @self, @identity, _dispatcher, _environment changed.
HookScopeBody: _environment changed.
HookScopeBody: _environment changed.

The problem is in observing Environment(\.self) which invalidates HookScopeBody's body for each change in environment, causing a view to re-render.

I haven't dig into finding a solution yet, but wanted to keep the issue on sight as it can potentially cause unexpected re-renders of the whole app.

Notes:

  • ⚠️ HookView is also affected (as it internally uses HookScope)
  • ⚠️ When working on a solution, we need to keep in mind that Context internally uses environments too.

Expected Behavior

HookScope body should only invalidate for key path specified in useEnvironment or types used in useContext.

Reproduction Steps

  1. Create a new project and replace @main struct with _App struct from above
  2. Replace the __App__ with SwiftUIApp() and run
  3. Replace SwiftUIApp() call with HooksApp()
  4. Compare console outputs

Swift Version

5.6+

Library Version

<= 0.0.8

Platform

No response

Scrrenshot/Video/Gif

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions