diff --git a/README.md b/README.md index 3f514961c785..784a08fd82b7 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ SwiftUI, UIKit, and more, and on any Apple platform (iOS, macOS, iPadOS, visionO * [Examples](#examples) * [Basic usage](#basic-usage) * [Documentation](#documentation) +* [FAQ](#faq) * [Community](#community) * [Installation](#installation) * [Translations](#translations) @@ -575,6 +576,11 @@ comfortable with the library: * [Concurrency][concurrency-article] * [Bindings][bindings-article] +## FAQ + +We have a [dedicated article][faq-article] for all of the most frequently asked questions and +comments people have concerning the library. + ## Community 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 PR](https://github.com/pointfreeco/swift-composable-architecture/edit/main/README.md) with a link to a [Gist](https://gist.github.com)! -## FAQ - -We have a [dedicated article][faq-article] for all of the most frequently asked questions and -comments people have concerning the library. - ## Credits and thanks The following people gave feedback on the library at its early stages and helped make the library diff --git a/Sources/ComposableArchitecture/Documentation.docc/Articles/SharingState.md b/Sources/ComposableArchitecture/Documentation.docc/Articles/SharingState.md index d767728fe6d9..eb2a5be0c7bd 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Articles/SharingState.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Articles/SharingState.md @@ -33,6 +33,7 @@ system, such as SQLite. * [Observing changes to shared state](#Observing-changes-to-shared-state) * [Initialization rules](#Initialization-rules) * [Deriving shared state](#Deriving-shared-state) +* [Concurrent mutations to shared state](#Concurrent-mutations-to-shared-state) * [Testing shared state](#Testing-shared-state) * [Testing when using persistence](#Testing-when-using-persistence) * [Testing when using custom persistence strategies](#Testing-when-using-custom-persistence-strategies) @@ -41,7 +42,6 @@ system, such as SQLite. * [Testing tips](#Testing-tips) * [Read-only shared state](#Read-only-shared-state) * [Type-safe keys](#Type-safe-keys) -* [Concurrent mutations to shared state](#Concurrent-mutations-to-shared-state) * [Shared state in pre-observation apps](#Shared-state-in-pre-observation-apps) * [Gotchas of @Shared](#Gotchas-of-Shared) @@ -446,6 +446,39 @@ else { return } todo // Shared ``` +## Concurrent mutations to shared state + +[mutating-shared-state-article]: https://swiftpackageindex.com/pointfreeco/swift-sharing/main/documentation/sharing/mutatingsharedstate + +While the `@Shared` property wrapper makes it possible to treat shared state +_mostly_ like regular state, you do have to perform some extra steps to mutate shared state. +This is because shared state is technically a reference deep down, even +though we take extra steps to make it appear value-like. And this means it's possible to mutate the +same piece of shared state from multiple threads, and hence race conditions are possible. See +[Mutating Shared State][mutating-shared-state-article] for a more in-depth explanation. + +To mutate a piece of shared state in an isolated fashion, use the `withLock` method +defined on the `@Shared` projected value: + +```swift +state.$count.withLock { $0 += 1 } +``` + +That locks the entire unit of work of reading the current count, incrementing it, and storing it +back in the reference. + +Technically it is still possible to write code that has race conditions, such as this silly example: + +```swift +let currentCount = state.count +state.$count.withLock { $0 = currentCount + 1 } +``` + +But there is no way to 100% prevent race conditions in code. Even actors are susceptible to +problems due to re-entrancy. To avoid problems like the above we recommend wrapping as many +mutations of the shared state as possible in a single `withLock`. That will make +sure that the full unit of work is guarded by a lock. + ## Testing shared state Shared state behaves quite a bit different from the regular state held in Composable Architecture @@ -476,7 +509,7 @@ struct Feature { Reduce { state, action in switch action { case .incrementButtonTapped: - state.count += 1 + state.$count.withLock { $0 += 1 } return .none } } @@ -484,7 +517,7 @@ struct Feature { } ``` -This feature can be tested in exactly the same way as when you are using non-shared state: +This feature can be tested in a similar same way as when you are using non-shared state: ```swift @Test @@ -494,7 +527,7 @@ func increment() async { } await store.send(.incrementButtonTapped) { - $0.count = 1 + $0.$count.withLock { $0 = 1 } } } ``` @@ -511,7 +544,7 @@ func increment() async { } await store.send(.incrementButtonTapped) { - $0.count = 2 + $0.$count.withLock { $0 = 2 } } } ``` @@ -590,7 +623,7 @@ func increment() async { } await store.send(.incrementButtonTapped) store.assert { - $0.count = 1 + $0.$count.withLock { $0 = 1 } } } ``` @@ -964,36 +997,6 @@ struct FeatureView: View { } ``` -## Concurrent mutations to shared state - -While the [`@Shared`]() property wrapper makes it possible to treat shared state -_mostly_ like regular state, you do have to perform some extra steps to mutate shared state. -This is because shared state is technically a reference deep down, even -though we take extra steps to make it appear value-like. And this means it's possible to mutate the -same piece of shared state from multiple threads, and hence race conditions are possible. - -To mutate a piece of shared state in an isolated fashion, use the `withLock` method -defined on the `@Shared` projected value: - -```swift -state.$count.withLock { $0 += 1 } -``` - -That locks the entire unit of work of reading the current count, incrementing it, and storing it -back in the reference. - -Technically it is still possible to write code that has race conditions, such as this silly example: - -```swift -let currentCount = state.count -state.$count.withLock { $0 = currentCount + 1 } -``` - -But there is no way to 100% prevent race conditions in code. Even actors are susceptible to -problems due to re-entrancy. To avoid problems like the above we recommend wrapping as many -mutations of the shared state as possible in a single `withLock`. That will make -sure that the full unit of work is guarded by a lock. - ## Gotchas of @Shared There are a few gotchas to be aware of when using shared state in the Composable Architecture.