GithubHelp home page GithubHelp logo

Comments (6)

mbrandonw avatar mbrandonw commented on August 22, 2024

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.

natemann avatar natemann commented on August 22, 2024

@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.

mayoff avatar mayoff commented on August 22, 2024

Change the action to include the item:

case addItemToCart(Item)

from episode-code-samples.

natemann avatar natemann commented on August 22, 2024

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.

JimRoepcke avatar JimRoepcke commented on August 22, 2024

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.

erikolsson avatar erikolsson commented on August 22, 2024

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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.