Access parent state in child for higher-order reducer #2483
Unanswered
madsodgaard
asked this question in
Q&A
Replies: 2 comments
-
@madsodgaard nice to see you here :) |
Beta Was this translation helpful? Give feedback.
0 replies
-
I came up with something like this, but I am really uncertain if this is how you would model something like this in TCA. Any input @mbrandonw ? import ComposableArchitecture
import IdentifiedCollections
import SwiftUI
import CasePaths
public struct PageContext: Equatable {
public let nextPageCursor: String?
public init(nextPageCursor: String?) {
self.nextPageCursor = nextPageCursor
}
}
public struct Page<Item> {
public let context: PageContext
public let items: [Item]
public init(context: PageContext, items: [Item]) {
self.context = context
self.items = items
}
public func map<T>(_ closure: (Item) throws -> T) rethrows -> Page<T> {
Page<T>(context: self.context, items: try self.items.map(closure))
}
}
extension Page: Equatable where Item: Equatable {}
public struct PageRequest: Equatable {
public let cursor: String?
public let itemsPerPage: Int
init(cursor: String?, itemsPerPage: Int) {
self.cursor = cursor
self.itemsPerPage = itemsPerPage
}
}
public struct PaginatorState<ItemState: Equatable & Identifiable>: Equatable {
public var items: IdentifiedArrayOf<ItemState>
var nextPageCursor: String?
var isLoading: Bool
var hasMoreData: Bool
public init(
items: IdentifiedArrayOf<ItemState> = []
) {
self.items = items
self.nextPageCursor = nil
self.isLoading = false
self.hasMoreData = true
}
}
public enum PaginatorAction<ItemState: Equatable & Identifiable, ItemAction: Equatable>: Equatable {
case itemAppeared(ItemState.ID)
case resetAndLoad
case response(TaskResult<Page<ItemState>>)
case requestPage(PageRequest)
case item(id: ItemState.ID, action: ItemAction)
}
private struct Paginator<ItemState: Equatable & Identifiable, ItemAction: Equatable>: Reducer {
typealias State = PaginatorState<ItemState>
typealias Action = PaginatorAction<ItemState, ItemAction>
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .itemAppeared(let itemID):
guard state.items.last?.id == itemID else { return .none }
return self.fetchNextPage(&state)
case .resetAndLoad:
state.nextPageCursor = nil
state.items = []
state.hasMoreData = true
return self.fetchNextPage(&state)
case .response(let result):
state.isLoading = false
guard case .success(let page) = result else {
// TODO: Error handling
return .none
}
state.items.append(contentsOf: page.items)
state.nextPageCursor = page.context.nextPageCursor
state.hasMoreData = page.context.nextPageCursor != nil
return .none
case .requestPage:
// Should be handled by `.paginator()` extension
return .none
case .item:
return .none
}
}
}
private func fetchNextPage(_ state: inout State) -> Effect<Action> {
guard !state.isLoading, state.hasMoreData else { return .none }
state.isLoading = true
return .send(.requestPage(PageRequest(cursor: state.nextPageCursor, itemsPerPage: 10)))
}
}
public struct PaginatorView<ItemState: Equatable & Identifiable, ItemAction: Equatable, Body: View>: View {
let store: Store<PaginatorState<ItemState>, PaginatorAction<ItemState, ItemAction>>
let content: (ItemState) -> Body
public init(
store: Store<PaginatorState<ItemState>, PaginatorAction<ItemState, ItemAction>>,
content: @escaping (ItemState) -> Body
) {
self.store = store
self.content = content
}
public var body: some View {
WithViewStore(self.store, observe: { $0 }) { (viewStore: ViewStoreOf<Paginator<ItemState, ItemAction>>) in
ForEach(viewStore.items) { item in
content(item)
.onAppear {
viewStore.send(.itemAppeared(item.id))
}
}
if viewStore.isLoading {
ProgressView().progressViewStyle(.circular)
}
}
}
}
public struct PaginatorStore<ItemState: Equatable & Identifiable, ItemAction: Equatable, Body: View>: View {
let store: Store<PaginatorState<ItemState>, PaginatorAction<ItemState, ItemAction>>
let content: (Store<ItemState, ItemAction>) -> Body
public init(
store: Store<PaginatorState<ItemState>, PaginatorAction<ItemState, ItemAction>>,
content: @escaping (Store<ItemState, ItemAction>) -> Body
) {
self.store = store
self.content = content
}
public var body: some View {
WithViewStore(self.store, observe: { $0 }) { (viewStore: ViewStoreOf<Paginator<ItemState, ItemAction>>) in
ForEachStore(self.store.scope(state: \.items, action: PaginatorAction<ItemState, ItemAction>.item)) { itemStore in
content(itemStore)
.onAppear {
itemStore.withState {
_ = viewStore.send(.itemAppeared($0.id))
}
}
}
if viewStore.isLoading {
ProgressView().progressViewStyle(.circular)
}
}
}
}
// TODO: This could probably be cleaned up a lot.
// TODO: Do we even need PaginationState and Action?
private struct PaginationIntegrationReducer<
Parent: Reducer, ItemState: Equatable & Identifiable, ItemAction: Equatable
>: Reducer {
let parent: Parent
let toChildState: WritableKeyPath<Parent.State, PaginatorState<ItemState>>
let toChildAction: CasePath<Parent.Action, PaginatorAction<ItemState, ItemAction>>
let loadPage: @Sendable (PageRequest, Parent.State) async throws -> Page<ItemState>
var body: some Reducer<Parent.State, Parent.Action> {
Scope(state: toChildState, action: toChildAction) {
Paginator()
}
Reduce { state, action in
guard
let paginatorAction = toChildAction.extract(from: action),
case .requestPage(let pageRequest) = paginatorAction
else { return self.parent.reduce(into: &state, action: action) }
return .run { [state] send in
await send(toChildAction.embed(.response(TaskResult {
try await loadPage(pageRequest, state)
})))
}
}
}
}
extension Reducer {
/// A higher-order reducer that can be used for paginating API responses.
///
/// - Parameters:
/// - state: a keypath to ``PaginatorState``
/// - action: a casepath to ``PaginatorAction``
/// - loadPage: the method that is called when loading a new page, called with a copy of the parent state and the page to fetch.
public func paginator<ItemState: Equatable & Identifiable>(
state: WritableKeyPath<State, PaginatorState<ItemState>>,
action: CasePath<Action, PaginatorAction<ItemState, Never>>,
loadPage: @Sendable @escaping (PageRequest, State) async throws -> Page<ItemState>
) -> some Reducer<State, Action> {
PaginationIntegrationReducer(parent: self, toChildState: state, toChildAction: action, loadPage: loadPage)
}
/// A higher-order reducer that can be used for paginating API responses that require a child reducer
///
/// - Parameters:
/// - state: a keypath to ``PaginatorState``
/// - action: a casepath to ``PaginatorAction``
/// - loadPage: the method that is called when loading a new page, called with a copy of the parent state and the page to fetch.
public func paginator<ItemState: Equatable & Identifiable, ItemAction: Equatable, Element: Reducer>(
state: WritableKeyPath<State, PaginatorState<ItemState>>,
action: CasePath<Action, PaginatorAction<ItemState, ItemAction>>,
loadPage: @Sendable @escaping (PageRequest, State) async throws -> Page<ItemState>,
@ReducerBuilder<ItemState, ItemAction> element: () -> Element
) -> some Reducer<State, Action>
where Element.State == ItemState, Element.Action == ItemAction {
PaginationIntegrationReducer(parent: self, toChildState: state, toChildAction: action, loadPage: loadPage)
.forEach(state.appending(path: \.items), action: action.appending(path: /PaginatorAction.item(id:action:))) {
element()
}
}
} |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Hi! I am attempting to implement a higher-order reducer to handle pagination throughout my app. I've used the case study as inspiration for the implementation. But my case is a bit more complex. This is the code I've got so far:
And in my parent I implement the
Paginator
like soThe issue I run into now, is when I want to add additional information to the API call. Let's say that for example my parent can choose to filter on a specific name in the API endpoint. How would I pass that information along to the
loadPage()
method of myPaginator
reducer? Essentially I need someway to access the parent state when I call thelistQuestionPosts
method.Something like this:
Beta Was this translation helpful? Give feedback.
All reactions