🔥

Why Child @State Won't Update from Parent in SwiftUI

TL;DR: @State is designed to manage a view’s internal private state. Its initial value is only applied when the view establishes its Identity for the first time. To react to external data changes continuously, use @Binding or standard let properties instead.

The Phenomenon

Many SwiftUI beginners encounter a confusing scenario:

You have a child view with two parameters: one marked with @State and one without. When you update the data source in the parent view, only the property without @State updates in the child view. The @State property remains “stuck” on its initial value.

Swift
struct SubView: View {
    let normalValue: String
    @State var stateValue: String // This won't change when the parent updates
    
    // ...
}

The Core Semantics of @State

As a declarative reactive framework, SwiftUI relies on @State as its fundamental state management tool. Its core definition is singular:

The SwiftUI runtime is responsible for allocating and managing memory to ensure that the value held by @State shares a lifecycle completely tied to the view’s Identity.

This means @State explicitly defines a “private domain” property. Once storage is allocated for a view, that property is entirely managed internally by that view.

Swift
@State var name = "abc"
// Semantically equivalent to:
@State private var name = "abc"

Consequently, Apple’s documentation and best practices strongly recommend declaring @State as private to enforce this concept: it is the view’s private property and should not be interfered with by the outside world.

Why Can’t the Parent Modify It?

To enforce the semantics of @State, SwiftUI adopts a “one-time initialization” strategy in its underlying implementation.

  1. First Creation: When the view is loaded into the view tree for the first time (establishing Identity), SwiftUI initializes the internal storage (State<T>) using the parameter passed into the initializer (init).
  2. Subsequent Updates: When the parent view refreshes and calls the child view’s init method again, new parameters are passed. However, SwiftUI detects that storage for this view identity already exists. To maintain state continuity (e.g., preventing user input from being overwritten), SwiftUI ignores the new value passed to init and continues using the existing value in its internal storage.

Simply put, @State is like a box that only accepts a “housewarming gift” when you first move in. On every subsequent visit, it refuses to accept new gifts to protect what is already inside.

Deep Dive: If you are interested in the underlying implementation, check out Understanding the DynamicProperty Protocol. Internally, @State maintains a state enum that only accepts external assignment during specific initialization phases.

Solutions and Best Practices

  1. Distinguish Your Data Source:

    • If the data is owned and controlled by the parent, and the child simply displays it, use a standard let property.
    • If the child needs to modify the data and sync it back to the parent, use @Binding.
    • Use @State only when the data is strictly internal to the child view (e.g., button highlight state, temporary text input).
  2. Forced Refresh (Not Recommended):

    • If you absolutely must reset @State, you can change the view’s explicit identity using .id(value). This forces SwiftUI to destroy and recreate the view, triggering the initialization flow again. Be aware that this incurs performance costs and loss of other internal states.
  3. Reference Types (Modern Context):

    • Starting with iOS 17 and Swift 6, combined with the Observation framework, @State replaces @StateObject for holding Observable objects.
    • ⚠️ Performance Warning: Unlike @StateObject, @State does not have a Lazy Initialization mechanism. Avoid initializing expensive reference types directly in the View’s init or as default property values. Doing so causes the object to be created every time the view redraws (even though it is immediately discarded). For scenarios requiring lazy loading, consider using a custom @LazyState.
Swift
// ❌ Avoid this: HeavyObject is created on every redraw
@State private var store = HeavyObject() 

// ✅ Recommended: Initialize in onAppear or use a lazy pattern
Related Tips

Subscribe to Fatbobman

Weekly Swift & SwiftUI highlights. Join developers.

Subscribe Now