Comments (6)
There's a few approaches to this, and the approach you want to take depends on the domain.
For example, if it's fully ok for your subdomain (item) to make mutations to the global domain (cart), then you can use a custom computed property (which gives you a key path to pullback along) in order to share a bit of mutable state with the subdomain:
struct AppState {
let groceryItems: [Item]
var shoppingCart: [Item] = []
var selectedItemIdx: Int?
}
struct GroceryItemState {
let item: Item
// this state is a copy of what the parent domain has, and it will be
// shared with us.
var shoppingCart: [Item] = []
}
enum GroceryItemAction {
case addItemToCart
}
extension AppState {
// This custom computed property allows the item reducer to
// make changes to the cart. Any mutations it makes to the
// cart will automatically be reflected in the parent. Most importantly
// the subdomain has no idea where its array of shopping cart
// items are coming from, so it is still fully decoupled from the parent domain.
var groceryItemState: GroceryItemState? {
get {
self.selectedItemIdx.map {
GroceryItemState(
item: self.groceryItems[$0],
shoppingCart: shoppingCart
)
}
}
set {
self.shoppingCard = newValue.shoppingCart
}
}
}
Sometimes it is fully appropriate for the subdomain to make any mutations it wants to the state, and the above will enable that.
On the other hand, sometimes that isn't appropriate. In fact, in this case maybe we don't want to give the subdomain full rein to mutate the parent state, then we can instead allow the parent domain to intercept a child's action so that it can be responsible for doing the mutation:
struct AppState {
let groceryItems: [Item]
var shoppingCart: [Item] = []
var selectedItemIdx: Int?
}
enum AppAction {
case groceryItem(GroceryItemAction)
// ...and the rest of the app actions
}
struct GroceryItemState {
let item: Item
var shoppingCart: [Item] = []
}
enum GroceryItemAction {
case addItemToCart
}
func appReducer(state: inout AppState, action: AppAction) -> [Effect<AppAction>] {
switch action {
// handle all of the app actions like you normally would
case .groceryItem(.addItemToCart):
guard let idx = state.selectedItemIdx else { return [] }
state.shoppingCard.append(self.groceryItems[idx])
return []
}
}
Here the parent is responsible for seeing when the child had its addItemToCart
action sent, and then can do the mutation itself.
There are even more approaches for decoupling these situations, but these two are the easiest to describe in a short amount of time. It's also worth noting that the above do not cover all the details necessary for either approach. You will find more little bits that you need to figure out, but hopefully this sets you down a path to explore it more.
from episode-code-samples.
@mbrandonw, thank you, and this does help. I was going down the second path, and it was working, but hit a snag. Lets say instead of just adding an Item to the cart, we have this
struct AppState {
let groceryItems: [Item]
var shoppingCart: [OrderItem] = []
}
struct OrderItem {
let item: Item
var quantity: Int
}
struct GroceryItemState {
let orderItem: OrderItem
}
enum GroceryItemAction {
case setQuantity(Int)
case addItemToCart
}
With this structure, it seems the appReducer
will only work if AppState
has something like an inProgressOrderItem: OrderItem
struct AppState {
let groceryItems: [Item]
var shoppingCart: [OrderItem] = []
var inProgressOrderItem: OrderItem?
}
then the appReducer would look like
func appReducer(state: inout AppState, action: AppAction) -> [Effect<AppAction>] {
switch action {
// handle all of the app actions like you normally would
case .groceryItem(let action):
switch action {
case .setQuantity(let quantity)
state.inProgressOrderItem.quantity = quantity
case .addToCart:
state.shoppingCart.append(inProgressOrder)
inProgressOrder = nil
return nil
}
}
}
Can you think of a better way to do this? I feel like inProgressOrder
is 'intermediate' or 'temporary' state that maybe shouldn't be stored in AppState
from episode-code-samples.
Change the action to include the item:
case addItemToCart(Item)
from episode-code-samples.
this is the solution I have come up with so far...
I added a property to the store
public let upLift: (Action) -> Void
then, the send
function looks like this
public func send(_ action: Action) {
upLift(action)
let effects = self.reducer(&self.value, action)
effects.forEach { effect in
var effectCancellable: AnyCancellable?
var didComplete = false
effectCancellable = effect.sink(
receiveCompletion: { [weak self] _ in
didComplete = true
guard let effectCancellable = effectCancellable else { return }
self?.effectCancellables.remove(effectCancellable)
},
receiveValue: self.send
)
if !didComplete, let effectCancellable = effectCancellable {
self.effectCancellables.insert(effectCancellable)
}
}
}
finally, when initializing the store directly, not through the parent store's view
method, you can do something like this
Store.init(
initialState: //state/,
reducer: //reducer/,
liftUp: { action in
//deal with the action as needed
guard case let addItemToCart(item) else { return }
self.send(AppAction.addItemToCart(item)
})
initializing the Store directly allows the Store to have Actions and State properties that the GlobalAction and GlobalState do not know about. We only care about the uplifted action.
I don't know if this is right. Would love any and all feedback
from episode-code-samples.
Given this GroceryItemAction
:
enum GroceryItemAction {
case setCartQuantity(Item, Int)
case incrCartQuantity(Item)
case decrCartQuantity(Item)
}
and a case groceryItem(GroceryItemAction)
in AppAction
, appReducer
has all the information it needs to know which item's quantity to update without adding any special new functionality to the Store
.
func appReducer(state: inout AppState, action: AppAction) -> [Effect<AppAction>] {
switch action {
// handle all of the app actions like you normally would
case .groceryItem(.setCartQuantity(let item, let quantity)):
if let index = value.shoppingCartIndex(of: item) {
if quantity > 0 {
value.shoppingCart[index].quantity = quantity
} else {
value.shoppingCart.remove(at: index)
}
} else if quantity > 0 {
value.shoppingCart.append(OrderItem(item: item, quantity: quantity))
}
return []
case .groceryItem(.incrCartQuantity(let item)):
if let index = value.shoppingCartIndex(of: item) {
value.shoppingCart[index].quantity += 1
} else {
value.shoppingCart.append(OrderItem(item: item, quantity: 1))
}
return []
case .groceryItem(.decrCartQuantity(let item)):
if let index = value.shoppingCartIndex(of: item) {
value.shoppingCart[index].quantity -= 1
if value.shoppingCart[index].quantity < 1 {
value.shoppingCart.remove(at: index)
}
}
return []
}
}
extension AppState {
/// assuming `Item` is `Equatable`
func shoppingCartIndex(of item: Item) -> Int? {
shoppingCart.firstIndex(where: { $0.item == item })
}
}
from episode-code-samples.
Late to the party but an one approach that worked for my was to gather all side effects in a separate reducer, a bit like this:
func sideEffects(state: inout AppState, action: AppAction, environment: AppEnvironment) -> [Effect<AppAction>] {
switch action {
case let .editUser(.didUpdateUser(user)):
return [.just(AppAction.login(.setUser(user)))]
default:
break
}
return []
}
from episode-code-samples.
Related Issues (20)
- 0167 Navigation is weird on iPad HOT 1
- Please fix the error Episode #76 ~ HOT 2
- [0171-modularization-pt1] Setting Up HOT 3
- modularization folders architecture HOT 1
- Missing episode 187: URL Route? HOT 1
- Cannot convert value of type - Examples will not compile using Xcode 13.3 HOT 2
- Request: Re-Record Episode 100 to 103 and Update Source Code HOT 3
- Episode 196: Runtime warning work only in half of the cases HOT 1
- minor bug in episode 198 sample HOT 1
- Code sample for episode 220 is missing HOT 1
- Strange navigation on episode 220 HOT 1
- Episode 2 -> Fish playground -> Ambiguous use of 'init(utf8String:)'
- Episode 0228 - Composable Navigation: Destinations sample code not found HOT 1
- navigationDestination not triggered HOT 3
- Modern SwiftUI: Standups App HOT 1
- Episode 241 code not published HOT 2
- Episode 109 sheet show twice when item name changed HOT 1
- Typo in Navigation Example code
- @FocusState not working with new @BindableState API HOT 5
- UIApplication.shared.windows deprecated in iOS 15 HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from episode-code-samples.