Implement Unidirectional Flow with Store, Reducer, and Middleware
mainThe library implements a unidirectional data flow pattern using three core components:
- State: A struct representing the single source of truth for your feature.
- Action: An enum representing all possible events or intents.
- Reducer: A protocol that defines how the current
Stateis transformed into a newStategiven anAction. - Middleware: An actor that intercepts actions to perform side effects (like API calls) and returns new actions based on the results.
- Store: The central object that manages the state, applies reducers, and runs middlewares.
Implementation Example
// 1. Define State
struct SearchState: Equatable {
var repos: [Repo] = []
var isLoading = false
}
// 2. Define Actions
enum SearchAction: Equatable {
case search(query: String)
case setResults(repos: [Repo])
}
// 3. Implement Reducer
struct SearchReducer: Reducer {
func reduce(oldState: SearchState, with action: SearchAction) -> SearchState {
var state = oldState
switch action {
case .search:
state.isLoading = true
case let .setResults(repos):
state.repos = repos
state.isLoading = false
}
return state
}
}
// 4. Implement Middleware for side effects
actor SearchMiddleware: Middleware {
struct Dependencies {
var search: (String) async throws -> SearchResponse
// ... production implementation ...
}
let dependencies: Dependencies
init(dependencies: Dependencies) { self.dependencies = dependencies }
func process(state: SearchState, with action: SearchAction) async -> SearchAction? {
switch action {
case let .search(query):
let results = try? await dependencies.search(query)
return .setResults(repos: results?.items ?? [])
default:
return nil
}
}
}
// 5. Initialize the Store
typealias SearchStore = Store<SearchState, SearchAction>
let store = SearchStore(
initialState: .init(),
reducer: SearchReducer(),
middlewares: [SearchMiddleware(dependencies: .production)]
)struct SearchState: Equatable {
var repos: [Repo] = []
var isLoading = false
}
enum SearchAction: Equatable {
case search(query: String)
case setResults(repos: [Repo])
}
struct SearchReducer: Reducer {
func reduce(oldState: SearchState, with action: SearchAction) -> SearchState {
var state = oldState
switch action {
case .search:
state.isLoading = true
case let .setResults(repos):
state.repos = repos
state.isLoading = false
}
return state
}
}
actor SearchMiddleware: Middleware {
struct Dependencies {
var search: (String) async throws -> SearchResponse
static var production: Dependencies {
.init { query in
guard var urlComponents = URLComponents(string: "https://api.github.com/search/repositories") else {
return .init(items: [])
}
urlComponents.queryItems = [.init(name: "q", value: query)]
guard let url = urlComponents.url else {
return .init(items: [])
}
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(SearchResponse.self, from: data)
}
}
}
let dependencies: Dependencies
init(dependencies: Dependencies) {
self.dependencies = dependencies
}
func process(state: SearchState, with action: SearchAction) async -> SearchAction? {
switch action {
case let .search(query):
let results = try? await dependencies.search(query)
guard !Task.isCancelled else {
return .setResults(repos: state.repos)
}
return .setResults(repos: results?.items ?? [])
default:
return nil
}
}
}
typealias SearchStore = Store<SearchState, SearchAction>
struct SearchContainerView: View {
@State private var store = SearchStore(
initialState: .init(),
reducer: SearchReducer(),
middlewares: [SearchMiddleware(dependencies: .production)]
)
@State private var query = ""
var body: some View {
List(store.repos) { repo in
VStack(alignment: .leading) {
Text(verbatim: repo.name)
.font(.headline)
if let description = repo.description {
Text(verbatim: description)
}
}
}
.redacted(reason: store.isLoading ? .placeholder : [])
.searchable(text: $query)
.task(id: query) {
await store.send(.search(query: query))
}
.navigationTitle("Github Search")
}
}