Skip to content

Commit feaa4d0

Browse files
Fix docs for mutating shared state. (#3580)
* Fix docs for mutating shared state. * A few more fixes * Add FAQ to table of contents. * Update Sources/ComposableArchitecture/Documentation.docc/Articles/SharingState.md --------- Co-authored-by: Stephen Celis <[email protected]>
1 parent 688802c commit feaa4d0

File tree

2 files changed

+45
-41
lines changed

2 files changed

+45
-41
lines changed

README.md

+6-5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ SwiftUI, UIKit, and more, and on any Apple platform (iOS, macOS, iPadOS, visionO
1414
* [Examples](#examples)
1515
* [Basic usage](#basic-usage)
1616
* [Documentation](#documentation)
17+
* [FAQ](#faq)
1718
* [Community](#community)
1819
* [Installation](#installation)
1920
* [Translations](#translations)
@@ -575,6 +576,11 @@ comfortable with the library:
575576
* [Concurrency][concurrency-article]
576577
* [Bindings][bindings-article]
577578

579+
## FAQ
580+
581+
We have a [dedicated article][faq-article] for all of the most frequently asked questions and
582+
comments people have concerning the library.
583+
578584
## Community
579585

580586
If you want to discuss the Composable Architecture or have a question about how to use it to solve
@@ -639,11 +645,6 @@ If you'd like to contribute a translation, please [open a
639645
PR](https://github.com/pointfreeco/swift-composable-architecture/edit/main/README.md) with a link
640646
to a [Gist](https://gist.github.com)!
641647

642-
## FAQ
643-
644-
We have a [dedicated article][faq-article] for all of the most frequently asked questions and
645-
comments people have concerning the library.
646-
647648
## Credits and thanks
648649

649650
The following people gave feedback on the library at its early stages and helped make the library

Sources/ComposableArchitecture/Documentation.docc/Articles/SharingState.md

+39-36
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ system, such as SQLite.
3333
* [Observing changes to shared state](#Observing-changes-to-shared-state)
3434
* [Initialization rules](#Initialization-rules)
3535
* [Deriving shared state](#Deriving-shared-state)
36+
* [Concurrent mutations to shared state](#Concurrent-mutations-to-shared-state)
3637
* [Testing shared state](#Testing-shared-state)
3738
* [Testing when using persistence](#Testing-when-using-persistence)
3839
* [Testing when using custom persistence strategies](#Testing-when-using-custom-persistence-strategies)
@@ -41,7 +42,6 @@ system, such as SQLite.
4142
* [Testing tips](#Testing-tips)
4243
* [Read-only shared state](#Read-only-shared-state)
4344
* [Type-safe keys](#Type-safe-keys)
44-
* [Concurrent mutations to shared state](#Concurrent-mutations-to-shared-state)
4545
* [Shared state in pre-observation apps](#Shared-state-in-pre-observation-apps)
4646
* [Gotchas of @Shared](#Gotchas-of-Shared)
4747

@@ -446,6 +446,39 @@ else { return }
446446
todo // Shared<Todo>
447447
```
448448

449+
## Concurrent mutations to shared state
450+
451+
[mutating-shared-state-article]: https://swiftpackageindex.com/pointfreeco/swift-sharing/main/documentation/sharing/mutatingsharedstate
452+
453+
While the `@Shared` property wrapper makes it possible to treat shared state
454+
_mostly_ like regular state, you do have to perform some extra steps to mutate shared state.
455+
This is because shared state is technically a reference deep down, even
456+
though we take extra steps to make it appear value-like. And this means it's possible to mutate the
457+
same piece of shared state from multiple threads, and hence race conditions are possible. See
458+
[Mutating Shared State][mutating-shared-state-article] for a more in-depth explanation.
459+
460+
To mutate a piece of shared state in an isolated fashion, use the `withLock` method
461+
defined on the `@Shared` projected value:
462+
463+
```swift
464+
state.$count.withLock { $0 += 1 }
465+
```
466+
467+
That locks the entire unit of work of reading the current count, incrementing it, and storing it
468+
back in the reference.
469+
470+
Technically it is still possible to write code that has race conditions, such as this silly example:
471+
472+
```swift
473+
let currentCount = state.count
474+
state.$count.withLock { $0 = currentCount + 1 }
475+
```
476+
477+
But there is no way to 100% prevent race conditions in code. Even actors are susceptible to
478+
problems due to re-entrancy. To avoid problems like the above we recommend wrapping as many
479+
mutations of the shared state as possible in a single `withLock`. That will make
480+
sure that the full unit of work is guarded by a lock.
481+
449482
## Testing shared state
450483

451484
Shared state behaves quite a bit different from the regular state held in Composable Architecture
@@ -476,15 +509,15 @@ struct Feature {
476509
Reduce { state, action in
477510
switch action {
478511
case .incrementButtonTapped:
479-
state.count += 1
512+
state.$count.withLock { $0 += 1 }
480513
return .none
481514
}
482515
}
483516
}
484517
}
485518
```
486519

487-
This feature can be tested in exactly the same way as when you are using non-shared state:
520+
This feature can be tested in a similar same way as when you are using non-shared state:
488521

489522
```swift
490523
@Test
@@ -494,7 +527,7 @@ func increment() async {
494527
}
495528

496529
await store.send(.incrementButtonTapped) {
497-
$0.count = 1
530+
$0.$count.withLock { $0 = 1 }
498531
}
499532
}
500533
```
@@ -511,7 +544,7 @@ func increment() async {
511544
}
512545

513546
await store.send(.incrementButtonTapped) {
514-
$0.count = 2
547+
$0.$count.withLock { $0 = 2 }
515548
}
516549
}
517550
```
@@ -590,7 +623,7 @@ func increment() async {
590623
}
591624
await store.send(.incrementButtonTapped)
592625
store.assert {
593-
$0.count = 1
626+
$0.$count.withLock { $0 = 1 }
594627
}
595628
}
596629
```
@@ -964,36 +997,6 @@ struct FeatureView: View {
964997
}
965998
```
966999

967-
## Concurrent mutations to shared state
968-
969-
While the [`@Shared`](<doc:Shared>) property wrapper makes it possible to treat shared state
970-
_mostly_ like regular state, you do have to perform some extra steps to mutate shared state.
971-
This is because shared state is technically a reference deep down, even
972-
though we take extra steps to make it appear value-like. And this means it's possible to mutate the
973-
same piece of shared state from multiple threads, and hence race conditions are possible.
974-
975-
To mutate a piece of shared state in an isolated fashion, use the `withLock` method
976-
defined on the `@Shared` projected value:
977-
978-
```swift
979-
state.$count.withLock { $0 += 1 }
980-
```
981-
982-
That locks the entire unit of work of reading the current count, incrementing it, and storing it
983-
back in the reference.
984-
985-
Technically it is still possible to write code that has race conditions, such as this silly example:
986-
987-
```swift
988-
let currentCount = state.count
989-
state.$count.withLock { $0 = currentCount + 1 }
990-
```
991-
992-
But there is no way to 100% prevent race conditions in code. Even actors are susceptible to
993-
problems due to re-entrancy. To avoid problems like the above we recommend wrapping as many
994-
mutations of the shared state as possible in a single `withLock`. That will make
995-
sure that the full unit of work is guarded by a lock.
996-
9971000
## Gotchas of @Shared
9981001

9991002
There are a few gotchas to be aware of when using shared state in the Composable Architecture.

0 commit comments

Comments
 (0)