StevenLambion / SwiftDux

Predictable state management for SwiftUI applications.
http://stevenlambion.github.io/SwiftDux
MIT License
153 stars 9 forks source link

ActionPlan chaining #18

Closed hfabisiak closed 4 years ago

hfabisiak commented 4 years ago

Hi! Is there a way that I can chain ActionPlan requests? Next action plan gets executed when all actions from the previous one get executed. For example:

TodoAction.fetchTodos() .then(TodoAction.filterTodos()) .then(TodoAction.mapTodos())

StevenLambion commented 4 years ago

Originally, I was planning to add functionality similar to redux thunks as a native API, which could provide this kind of feature. It ended up causing a lot of complexity, and opened the door for abuse of the unidirectional data-flow.

The way I view it is that an action is simply a dispatched event. The dispatcher shouldn't care what happens to the acton or if it ever modifies the state. Observers of the state should be used instead to trigger any new action that might be needed.

For the example you provided, I might handle fetchTodos() in the following ways:

  1. It would populate the state after it filters and maps the data internally.
  2. The reducer might filter and map the todos before adding them to the state. It could create a hashed dictionary for all todos by their ids, and then another collection of ids representing the filtered ones.
  3. It would populate the state with a collection of all todos. This would trigger the relevant view to update, which would filter its own set of todos when it maps them from the state. It could use a selector to cache the results.
  4. An optional closure could be passed to fetchTodos() that provides a derived publisher from the service call. It could be used to further perform any extra operations on the todos before they're added to the state:

    let action = fetchTodos { (todos: AnyPublisher<[Todo], Error>) in
    todos.filter { ... }.map { ... } // The returning publisher is then mapped to a `setTodos` action.
    }
    
    dispatch(action)
StevenLambion commented 4 years ago

It might be possible to pull this off inside a containing publishable action plan. It's kind of a hack, but you could observe off of the store.didChange property using something like firstWhere(_:) to wait for the fetchTodos() to update the state. The store.state property inside an action plan always provides the latest snapshot of the state similar to getState() in redux thunk.

StevenLambion commented 4 years ago

I'm working on a solution using a new API on the ActionPlan itself:

let chainedActions = firstActionPlan.then(secondActionPlan).then(thirdActionPlan)

// Or

let chainedActions = firstActionPlan.next(secondActionPlan, thirdActionPlan)

Each plan would wait for the previous one to fully complete or be cancelled before running (Useful for plans that return a publisher). The method would return a new version of the first action plan that internally keeps a copy of the next ones to run after it finishes.

There's no data passing between the action plans. Instead, the store's state can be accessed to retrieve anything necessary.

StevenLambion commented 4 years ago

Added the feature in v0.12.0