Skip to content

Commit

Permalink
Better document explicit id with scoped ForEach (#2784)
Browse files Browse the repository at this point in the history
`ForEachStore` implicitly used `id: \.state.id` under the hood, so a
naive migration to `ForEach` might miss this detail. Let's be more
explicit in our documentation and migration guide to avoid this issue.

In the future it might be possible to address better in the library
itself, but for now the requirement for stores to be identifiable for
other forms of navigation means allowing for this.
  • Loading branch information
stephencelis authored Feb 8, 2024
1 parent 1e7942c commit d9a3cfb
Show file tree
Hide file tree
Showing 4 changed files with 33 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -204,27 +204,33 @@ struct Feature {
Then you would have made use of ``ForEachStore`` in the view like this:

```swift
ForEachStore(store.scope(state: \.rows, action: \.rows)) { childStore in
ForEachStore(
store.scope(state: \.rows, action: \.rows)
) { childStore in
ChildView(store: childStore)
}
```

This can now be updated to use the vanilla `ForEach` view in SwiftUI, along with
``Store/scope(state:action:)-1nelp``:
``Store/scope(state:action:)-1nelp``, identified by the state of each row:

```swift
ForEach(store.scope(state: \.rows, action: \.rows)) { childStore in
ForEach(
store.scope(state: \.rows, action: \.rows), id: \.state.id
) { childStore in
ChildView(store: childStore)
}
```

If your usage of `ForEachStore` relied on the identity of the state of each row (_e.g._, the state's
`id` is also associated with a selection binding), you must explicitly use the `id` parameter:
If your usage of `ForEachStore` did not depend on the identity of the state of each row (_e.g._, the
state's `id` is not associated with a selection binding), you can omit the `id` parameter, as the
`Store` type is identifiable by its object identity:

```diff
ForEach(
store.scope(state: \.rows, action: \.rows),
+ id: \.state.id
- store.scope(state: \.rows, action: \.rows),
- id: \.state.id,
+ store.scope(state: \.rows, action: \.rows)
) { childStore in
ChildView(store: childStore)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ This means that even if you wrap the body of the view in `WithPerceptionTracking

```swift
WithPerceptionTracking {
ForEach(store.scope(state: \.rows, action: \.rows) { store in
ForEach(store.scope(state: \.rows, action: \.rows), id: \.state.id) { store in
Text(store.title)
}
}
Expand All @@ -84,7 +84,7 @@ The fix for this is to wrap the content of the trailing closure in another `With

```swift
WithPerceptionTracking {
ForEach(store.scope(state: \.rows, action: \.rows) { store in
ForEach(store.scope(state: \.rows, action: \.rows), id: \.state.id) { store in
WithPerceptionTracking {
Text(store.title)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ Another example is scoping to some collection of a child domain in order to use
``ForEachStore``:

```swift
ForEachStore(store.scope(state: \.rows, action: \.rows) { store in
ForEachStore(store.scope(state: \.rows, action: \.rows)) { store in
RowView(store: store)
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
extension Store where State: ObservableState {
/// Scopes the store of an identified collection to a collection of stores.
///
/// This operator is most often used with SwiftUI's `ForEach` view. For example, suppose you have
/// a feature that contains an `IdentifiedArray` of child features like so:
/// This operator is most often used with SwiftUI's `ForEach` view. For example, suppose you
/// have a feature that contains an `IdentifiedArray` of child features like so:
///
/// ```swift
/// @Reducer
Expand Down Expand Up @@ -38,14 +38,28 @@
///
/// var body: some View {
/// List {
/// ForEach(store.scope(state: \.rows, action: \.rows) { store in
/// ForEach(store.scope(state: \.rows, action: \.rows), id: \.state.id) { store in
/// ChildView(store: store)
/// }
/// }
/// }
/// }
/// ```
///
/// > Tip: If you do not depend on the identity of the state of each row (_e.g._, the state's
/// > `id` is not associated with a selection binding), you can omit the `id` parameter, as the
/// > `Store` type is identifiable by its object identity:
/// >
/// > ```diff
/// > ForEach(
/// > - store.scope(state: \.rows, action: \.rows),
/// > - id: \.state.id,
/// > + store.scope(state: \.rows, action: \.rows)
/// > ) { childStore in
/// > ChildView(store: childStore)
/// > }
/// > ```
///
/// - Parameters:
/// - state: A key path to an identified array of child state.
/// - action: A case key path to an identified child action.
Expand Down

0 comments on commit d9a3cfb

Please sign in to comment.