{"id":18264734,"url":"https://github.com/reactcomponentkit/redux","last_synced_at":"2025-04-04T21:30:48.890Z","repository":{"id":42389531,"uuid":"335901170","full_name":"ReactComponentKit/Redux","owner":"ReactComponentKit","description":"Manage iOS App state with Redux and Async/Await :)","archived":false,"fork":false,"pushed_at":"2022-04-07T13:30:00.000Z","size":691,"stargazers_count":25,"open_issues_count":2,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-20T19:17:16.440Z","etag":null,"topics":["async","await","ios","redux","swiftui","uikit"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ReactComponentKit.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-02-04T09:21:37.000Z","updated_at":"2025-03-07T19:44:01.000Z","dependencies_parsed_at":"2022-09-19T17:32:33.543Z","dependency_job_id":null,"html_url":"https://github.com/ReactComponentKit/Redux","commit_stats":null,"previous_names":[],"tags_count":34,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ReactComponentKit%2FRedux","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ReactComponentKit%2FRedux/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ReactComponentKit%2FRedux/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ReactComponentKit%2FRedux/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ReactComponentKit","download_url":"https://codeload.github.com/ReactComponentKit/Redux/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247252019,"owners_count":20908611,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","await","ios","redux","swiftui","uikit"],"created_at":"2024-11-05T11:15:46.361Z","updated_at":"2025-04-04T21:30:48.328Z","avatar_url":"https://github.com/ReactComponentKit.png","language":"Swift","readme":"[English](https://github.com/ReactComponentKit/Redux/blob/main/README.md) | [한국어](https://github.com/ReactComponentKit/Redux/blob/main/README_ko.md)\n\n# Redux\n\n![license MIT](https://img.shields.io/cocoapods/l/Redux.svg)\n![Platform](https://img.shields.io/badge/iOS-%3E%3D%2013.0-green.svg)\n![Platform](https://img.shields.io/badge/macos-%3E%3D%2010.15-green.svg)\n![Xcode](https://img.shields.io/badge/xcode-%3E%3D%2013.2-orange.svg)\n[![Swift 5.5](https://img.shields.io/badge/Swift-5.5-orange.svg?style=flat)](https://developer.apple.com/swift/)\n\nImplementing Redux with async/await introduced in Swift 5.5 has become very simple. From Xcode 13.2, Swift 5.5's new concurrency supports iOS 13. Therefore, the existing Redux package was newly implemented based on async/await.\n\n## Installation\n\nRedux only support Swift Package Manager. \n\n```swift\ndependencies: [\n    .package(url: \"https://github.com/ReactComponentKit/Redux.git\", from: \"1.2.1\"),\n]\n```\n\n## Flow\n\n![](./arts/flow.png)\n\nThe figure above shows the flow of Redux. There's a lot of content, but it's actually very concise. The store handles most of the flow. All the developer has to do is define State and Store and define the functions that perform Action and Mutation. Additionally, middleware-like jobs can be defined so that you can do the necessary tasks before or after Mutation occurs.\n\n## State\n\nState can be defined as below.\n\n```swift\nstruct Counter: State {\n    var count = 0\n}\n```\n\nNote that State should comply with Equatable.\n\n\n## Store\n\nWhen defining a store, a state is required. You can define the store as below.\n\n```swift\nstruct Counter: State {\n    var count = 0\n}\n\nclass CounterStore: Store\u003cCounter\u003e {\n    init() {\n        super.init(state: Counter())\n    }\n}\n```\n\nThe store provides the following methods.\n\n- commit(mutation:, payload:)\n- dispatch(action:, payload:) async\n- dispatch(action:, payload:)\n\nWhen creating a custom store, the method mainly used will be commit(mutation:, payload:). dispatch(action:, payload:) is very rarely used.\n\n## Mutation\n\nMutation is defined as a store method. The Mutation method is a sync method.\n\n```swift\n// mutation\nprivate func increment(counter: inout Counter, payload: Int) {\n    counter.count += payload\n}\n    \nprivate func decrement(counter: inout Counter, payload: Int) {\n    counter.count -= payload\n}\n```\n\n## Action\n\nAction is also defined by the store's method. There is no need to create a separate custom data type for action anymore. Since Action is defined as a store method, there are very few cases where the store's dispatch method is actually used. \n\nAction can be defined by dividing it into Sync Action or Async Action. Since the state change occurs in the commit, there is no need to define additional middleware for asynchronous processing. You can complete asynchronous processing in the async action and then commit the changes.\n\n```swift\n// actions\nfunc incrementAction(payload: Int) {\n    self.commit(mutation: increment, payload: payload)\n}\n    \nfunc decrementAction(payload: Int) {\n    self.commit(mutation: decrement, payload: payload)\n}\n    \nfunc asyncIncrementAction(payload: Int) async {\n    await Task.sleep(1 * 1_000_000_000)\n    self.commit(mutation: increment, payload: payload)\n}\n    \nfunc asyncDecrementAction(payload: Int) async {\n    await Task.sleep(1 * 1_000_000_000)\n    self.commit(mutation: decrement, payload: payload)\n}\n```\n\nAlso, You can use simplified commit method to define action or mutate state.\n\n```swift\nfunc asyncIncrementAction(payload: Int) async {\n    await Task.sleep(1 * 1_000_000_000)\n    self.commit { mutableState in \n        mutableState.count += 1\n    }\n}\n```\n\nStore's `commit` method is public so you can use it on the UI layer.\n\n```swift\nButton(action: { store.counter.commit { $0.count += 1 }) {\n    Text(\" + \")\n        .font(.title)\n        .bold()\n}\n```\n\nor use store's action method.\n\n```swift\nButton(action: { store.counter.incrementAction(payload: 1) }) {\n    Text(\" + \")\n        .font(.title)\n        .bold()\n}\n```\n\n\n## Computed\n\nDefine the properties to connect to View. The store does not publish the state. Therefore, in order to publish a specific property of the state, a value can be injected into the property in the computed step.\n\n```swift\nclass CounterStore: Store\u003cCounter\u003e {\n    init() {\n        super.init(state: Counter())\n    }\n    \n    // computed\n    @Published\n    var count = 0\n    \n    override func computed(new: Counter, old: Counter) {\n        self.count = new.count\n    }\n    ...\n}\n```\n\n## CounterStore\n\nThe entire code of CounterStore defined so far is as follows.\n\n```swift\nimport Foundation\nimport Redux\n\nstruct Counter: State {\n    var count = 0\n}\n\nclass CounterStore: Store\u003cCounter\u003e {\n    init() {\n        super.init(state: Counter())\n    }\n    \n    // computed\n    @Published\n    var count = 0\n    \n    override func computed(new: Counter, old: Counter) {\n        self.count = new.count\n    }\n    \n    // mutation\n    private func increment(counter: inout Counter, payload: Int) {\n        counter.count += payload\n    }\n    \n    private func decrement(counter: inout Counter, payload: Int) {\n        counter.count -= payload\n    }\n    \n    // actions\n    func incrementAction(payload: Int) {\n        self.commit(mutation: increment, payload: payload)\n    }\n    \n    func decrementAction(payload: Int) {\n        self.commit(mutation: decrement, payload: payload)\n    }\n    \n    func asyncIncrementAction(payload: Int) async {\n        await Task.sleep(1 * 1_000_000_000)\n        self.commit(mutation: increment, payload: payload)\n    }\n    \n    func asyncDecrementAction(payload: Int) async {\n        await Task.sleep(1 * 1_000_000_000)\n        self.commit(mutation: decrement, payload: payload)\n    }\n}\n```\n\n\n## Middlewares\n\nYou can optionally add middlewares. Middleware is a collection of functions called before or after all Mutations.\n\nYou can optionally add Middleware. Middleware is a collection of sync functions called before and after all mutations are commited. For example, you can define middleware that print logs to debug state changes.\n\n```swift\nclass WorksBeforeCommitStore: Store\u003cReduxState\u003e {\n    init() {\n        super.init(state: ReduxState())\n    }\n    \n    override func worksBeforeCommit() -\u003e [(ReduxState) -\u003e Void] {\n        return [\n            { (state) in\n                print(state.count)\n            }\n        ]\n    }\n}\n\nclass WorksAfterCommitStore: Store\u003cReduxState\u003e {\n    init() {\n        super.init(state: ReduxState())\n    }\n    \n    override func worksAfterCommit() -\u003e [(ReduxState) -\u003e Void] {\n        return [\n            { (state) in\n                print(state.count)\n            }\n        ]\n    }\n}\n```\n\n## UnitTest\n\nIt is very easy to test the CounterStore defined above.\n\n```swift\nimport XCTest\n@testable import Redux\n\nfinal class CounterStoreTests: XCTestCase {\n    private var store: CounterStore!\n    \n    override func setUp() {\n        super.setUp()\n        store = CounterStore()\n    }\n    \n    override func tearDown() {\n        super.tearDown()\n        store = nil\n    }\n    \n    func testInitialState() {\n        XCTAssertEqual(0, store.state.count)\n    }\n    \n    func testIncrementAction() {\n        store.incrementAction(payload: 1)\n        XCTAssertEqual(1, store.state.count)\n        store.incrementAction(payload: 10)\n        XCTAssertEqual(11, store.state.count)\n    }\n    \n    func testPublisherValue() {\n        XCTAssertEqual(0, store.count)\n        store.incrementAction(payload: 1)\n        XCTAssertEqual(1, store.count)\n        store.incrementAction(payload: 10)\n        XCTAssertEqual(11, store.count)\n        store.decrementAction(payload: 10)\n        XCTAssertEqual(1, store.count)\n        store.decrementAction(payload: 1)\n        XCTAssertEqual(0, store.count)\n    }\n    \n    func testAsyncIncrementAction() async {\n        await store.asyncIncrementAction(payload: 1)\n        XCTAssertEqual(1, store.state.count)\n        XCTAssertEqual(1, store.count)\n        await store.asyncIncrementAction(payload: 10)\n        XCTAssertEqual(11, store.state.count)\n        XCTAssertEqual(11, store.count)\n    }\n    \n    func testAsyncDecrementAction() async {\n        await store.asyncDecrementAction(payload: 1)\n        XCTAssertEqual(-1, store.state.count)\n        XCTAssertEqual(-1, store.count)\n        await store.asyncDecrementAction(payload: 10)\n        XCTAssertEqual(-11, store.state.count)\n        XCTAssertEqual(-11, store.count)\n    }\n}\n```\n\n## UserStore\n\nLet's define a store that uses API([https://jsonplaceholder.typicode.com](https://jsonplaceholder.typicode.com)).\n\n```swift\nimport Foundation\nimport Redux\n\nstruct User: Equatable, Codable {\n    let id: Int\n    var name: String\n}\n\nstruct UserState: State {\n    var users: [User] = []\n}\n\nclass UserStore: Store\u003cUserState\u003e {\n    \n    init() {\n        super.init(state: UserState())\n    }\n    \n    // mutations\n    private func SET_USERS(userState: inout UserState, payload: [User]) {\n        userState.users = payload\n    }\n    \n    private func SET_USER(userState: inout UserState, payload: User) {\n        let index = userState.users.firstIndex { it in\n            it.id == payload.id\n        }\n        \n        if let index = index {\n            userState.users[index] = payload\n        }\n    }\n    \n    // actions\n    func loadUsers() async {\n        do {\n            let (data, _) = try await URLSession.shared.data(from: URL(string: \"https://jsonplaceholder.typicode.com/users/\")!)\n            let users = try JSONDecoder().decode([User].self, from: data)\n            commit(mutation: SET_USERS, payload: users)\n        } catch {\n            print(#function, error)\n            commit(mutation: SET_USERS, payload: [])\n        }\n    }\n    \n    func update(user: User) async throws {\n        let params = try JSONEncoder().encode(user)\n        var request = URLRequest(url: URL(string: \"https://jsonplaceholder.typicode.com/users/\\(user.id)\")!)\n        request.httpMethod = \"PUT\"\n        request.httpBody = params\n        request.addValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")\n        request.addValue(\"application/json\", forHTTPHeaderField: \"Accept\")\n        let (data, _) = try await URLSession.shared.data(for: request)\n        let user = try JSONDecoder().decode(User.self, from: data)\n        commit(mutation: SET_USER, payload: user)\n    }\n}\n```\n\nYou can test the above UserStore as follows.\n\n```swift\nimport XCTest\n@testable import Redux\n\nfinal class UserStoreTests: XCTestCase {\n    private var store: UserStore!\n    \n    override func setUp() {\n        super.setUp()\n        store = UserStore()\n    }\n    \n    override func tearDown() {\n        super.tearDown()\n        store = nil\n    }\n    \n    func testInitialState() {\n        XCTAssertEqual([], store.state.users)\n    }\n    \n    func testLoadUsers() async {\n        await store.loadUsers()\n        XCTAssertEqual(10, store.state.users.count)\n        for user in store.state.users {\n            XCTAssertGreaterThan(user.id, 0)\n            XCTAssertNotEqual(user.name, \"\")\n        }\n    }\n \n    func testUpdateUser() async {\n        do {\n            await store.loadUsers()\n            XCTAssertEqual(10, store.state.users.count)\n            var mutableUser = store.state.users[0]\n            mutableUser.name = \"Sungcheol Kim\"\n            try await store.update(user: mutableUser)\n            XCTAssertEqual(10, store.state.users.count)\n            let user = store.state.users[0]\n            XCTAssertEqual(\"Sungcheol Kim\", user.name)\n        } catch {\n            XCTFail(\"Failed update user\")\n        }\n    }\n}\n```\n\n## Store Composition\n\nIt is necessary to manage the app status in one place with Single Source of Truth. In that case, it is dangerous to define all states of the app in one state. Therefore, it is recommended to divide the state into modules and create and manage a store that manages each state. You can define the App Store as below.\n\n\n```swift\nimport Foundation\nimport Redux\n\nstruct AppState: State {\n}\n\nclass AppStore: Store\u003cAppState\u003e {\n    \n    // composition store\n    let counter = CounterStore()\n    let users = UserStore()\n    \n    init() {\n        super.init(state: AppState())\n    }\n}\n```\n\nYou can use the AppStore above as follows.\n\n```swift\nimport XCTest\n@testable import Redux\n\n// Single Source of Truth\nfinal class SSOTTests: XCTestCase {\n    private var store: AppStore!\n    \n    override func setUp() {\n        super.setUp()\n        store = AppStore()\n    }\n    \n    override func tearDown() {\n        super.tearDown()\n        store = nil\n    }\n    \n    func testLoadUsers() async {\n        await store.users.loadUsers()\n        XCTAssertEqual(10, store.users.state.users.count)\n        for user in store.users.state.users {\n            XCTAssertGreaterThan(user.id, 0)\n            XCTAssertNotEqual(user.name, \"\")\n        }\n    }\n \n    func testUpdateUser() async {\n        do {\n            await store.users.loadUsers()\n            XCTAssertEqual(10, store.users.state.users.count)\n            var mutableUser = store.users.state.users[0]\n            mutableUser.name = \"Sungcheol Kim\"\n            try await store.users.update(user: mutableUser)\n            XCTAssertEqual(10, store.users.state.users.count)\n            let user = store.users.state.users[0]\n            XCTAssertEqual(\"Sungcheol Kim\", user.name)\n        } catch {\n            XCTFail(\"Failed update user\")\n        }\n    }\n    \n    func testIncrementAction() {\n        store.counter.incrementAction(payload: 1)\n        XCTAssertEqual(1, store.counter.state.count)\n        store.counter.incrementAction(payload: 10)\n        XCTAssertEqual(11, store.counter.state.count)\n    }\n    \n    func testPublisherValue() {\n        XCTAssertEqual(0, store.counter.count)\n        store.counter.incrementAction(payload: 1)\n        XCTAssertEqual(1, store.counter.count)\n        store.counter.incrementAction(payload: 10)\n        XCTAssertEqual(11, store.counter.count)\n        store.counter.decrementAction(payload: 10)\n        XCTAssertEqual(1, store.counter.count)\n        store.counter.decrementAction(payload: 1)\n        XCTAssertEqual(0, store.counter.count)\n    }\n    \n    func testAsyncIncrementAction() async {\n        await store.counter.asyncIncrementAction(payload: 1)\n        XCTAssertEqual(1, store.counter.state.count)\n        XCTAssertEqual(1, store.counter.count)\n        await store.counter.asyncIncrementAction(payload: 10)\n        XCTAssertEqual(11, store.counter.state.count)\n        XCTAssertEqual(11, store.counter.count)\n    }\n    \n    func testAsyncDecrementAction() async {\n        await store.counter.asyncDecrementAction(payload: 1)\n        XCTAssertEqual(-1, store.counter.state.count)\n        XCTAssertEqual(-1, store.counter.count)\n        await store.counter.asyncDecrementAction(payload: 10)\n        XCTAssertEqual(-11, store.counter.state.count)\n        XCTAssertEqual(-11, store.counter.count)\n    }\n}\n```\n\n## Example Of Store Composition\n\nWe can define the AppStore like as below but it is not a good design. If you add more state to the AppState, the AppStore becomes more massive store.\n\n```swift\nstruct AppState: State {\n    var count: Int = 0\n    var content: String? = nil\n    var error: String? = nil\n}\n\nclass AppStore: Store\u003cAppState\u003e {\n    init() {\n        super.init(state: AppState())\n    }\n    \n    @Published\n    var count: Int = 0\n    \n    @Published\n    var content: String? = nil\n    \n    @Published\n    var error: String? = nil\n    \n    override func computed(new: AppState, old: AppState) {\n        if (self.count != new.count) {\n            self.count = new.count\n        }\n        \n        if (self.content != new.content) {\n            self.content = new.content\n        }\n        \n        if (self.error != new.error) {\n            self.error = new.error\n        }\n    }\n    \n    override func worksAfterCommit() -\u003e [(AppState) -\u003e Void] {\n        return [ { state in\n            print(state.count)\n        }]\n    }\n    \n    private func INCREMENT(state: inout AppState, payload: Int) {\n        state.count += payload\n    }\n    \n    private func DECREMENT(state: inout AppState, payload: Int) {\n        state.count -= payload\n    }\n    \n    private func SET_CONTENT(state: inout AppState, payload: String) {\n        state.content = payload\n    }\n    \n    private func SET_ERROR(state: inout AppState, payload: String?) {\n        state.error = payload\n    }\n    \n    func incrementAction(payload: Int) {\n        commit(mutation: INCREMENT, payload: payload)\n    }\n    \n    func decrementAction(payload: Int) {\n        commit(mutation: DECREMENT, payload: payload)\n    }\n\n    func fetchContent() async {\n        do {\n            let (data, _) = try await URLSession.shared.data(from: URL(string: \"https://www.facebook.com\")!)\n            let value = String(data: data, encoding: .utf8) ?? \"\"\n            commit(mutation: SET_ERROR, payload: nil)\n            commit(mutation: SET_CONTENT, payload: value)\n        } catch {\n            commit(mutation: SET_ERROR, payload: error.localizedDescription)\n        }\n    }\n}\n```\n\nIt is a good practice to break the state into small pieces and then compose them to one store.\n\n```swift\n\n/**\n * CounterStore.swift\n */\nstruct Counter: State {\n    var count: Int = 0\n}\n\nclass CounterStore: Store\u003cCounter\u003e {\n    @Published\n    var count: Int = 0\n    \n    override func computed(new: Counter, old: Counter) {\n        if (self.count != new.count) {\n            self.count = new.count\n        }\n    }\n    \n    init() {\n        super.init(state: Counter())\n    }\n    \n    override func worksAfterCommit() -\u003e [(Counter) -\u003e Void] {\n        return [ { state in\n            print(state.count)\n        }]\n    }\n    \n    private func INCREMENT(state: inout Counter, payload: Int) {\n        state.count += payload\n    }\n    \n    private func DECREMENT(state: inout Counter, payload: Int) {\n        state.count -= payload\n    }\n    \n    func incrementAction(payload: Int) {\n        commit(mutation: INCREMENT, payload: payload)\n    }\n    \n    func decrementAction(payload: Int) {\n        commit(mutation: DECREMENT, payload: payload)\n    }\n}\n\n/**\n * ContentStore.swift\n */\nstruct Content: State {\n    var value: String? = nil\n    var error: String? = nil\n}\n\nclass ContentStore: Store\u003cContent\u003e {\n    @Published\n    var value: String? = nil\n    \n    @Published\n    var error: String? = nil\n    \n    override func computed(new: Content, old: Content) {\n        if (self.value != new.value) {\n            self.value = new.value\n        }\n        \n        if (self.error != new.error) {\n            self.error = new.error\n        }\n    }\n    \n    init() {\n        super.init(state: Content())\n    }\n    \n    override func worksAfterCommit() -\u003e [(Content) -\u003e Void] {\n        return [\n            { state in\n                print(state.value ?? \"없음\")\n            }\n        ]\n    }\n    \n    private func SET_CONTENT_VALUE(state: inout Content, payload: String) {\n        state.value = payload\n    }\n    \n    private func SET_ERROR(state: inout Content, payload: String?) {\n        state.error = payload\n    }\n    \n    func fetchContentValue() async {\n        do {\n            let (data, _) = try await URLSession.shared.data(from: URL(string: \"https://www.facebook.com\")!)\n            let value = String(data: data, encoding: .utf8) ?? \"\"\n            commit(mutation: SET_ERROR, payload: nil)\n            commit(mutation: SET_CONTENT_VALUE, payload: value)\n        } catch {\n            commit(mutation: SET_ERROR, payload: error.localizedDescription)\n        }\n    }\n}\n\n/**\n * ComposeAppStore.swift\n */\nstruct ComposeAppState: State {\n    // A state that depends on the state of another store.\n    var allLength: String = \"\"\n}\n\nclass ComposeAppStore: Store\u003cComposeAppState\u003e {\n    let counter = CounterStore();\n    let content = ContentStore();\n    \n    // Set it to private to access counter.count with the counter namespace in the UI layer.\n    @Published\n    private var count = 0;\n    \n    @Published\n    private var contentValue: String? = nil;\n    \n    @Published\n    private var error: String? = nil;\n    \n    @Published\n    var allLength: String? = nil;\n    \n    override func computed(new: ComposeAppState, old: ComposeAppState) {\n        if (new.allLength != old.allLength) {\n            self.allLength = new.allLength\n        }\n    }\n    \n    init() {\n        super.init(state: ComposeAppState())\n        // @Published chaining is required.\n        counter.$count.assign(to: \u0026self.$count)\n        content.$value.assign(to: \u0026self.$contentValue)\n        content.$error.assign(to: \u0026self.$error)\n    }\n    \n    //Examples of actions and state mutations that depend on the state and actions of other stores are\n    private func SET_ALL_LENGTH(state: inout ComposeAppState, payload: String) {\n        state.allLength = payload\n    }\n    func someComposeAction() async {\n        await content.fetchContentValue()\n        commit(mutation: SET_ALL_LENGTH, payload: \"counter: \\(counter.state.count), content: \\(content.state.value?.count ?? 0)\")\n    }\n}\n\n/**\n * ContentView.swift\n */\nimport SwiftUI\n\nstruct ContentView: View {\n    \n    @EnvironmentObject\n    private var store: ComposeAppStore\n    \n    var body: some View {\n        \n        VStack {\n            Text(\"\\(store.counter.count)\")\n                .font(.title)\n                .bold()\n                .padding()\n            if let error = store.content.error {\n                Text(\"Error! \\(error)\")\n            }\n            HStack {\n                Spacer()\n                \n                Button(action: { store.counter.decrementAction(payload: 1) }) {\n                    Text(\" - \")\n                        .font(.title)\n                        .bold()\n                }\n                \n                Spacer()\n                \n                Button(action: { store.counter.incrementAction(payload: 1) }) {\n                    Text(\" + \")\n                        .font(.title)\n                        .bold()\n                }\n                \n                Spacer()\n                \n            }\n            VStack {\n                Button(action: {\n                    Task {\n                        await store.someComposeAction()\n                    }\n                }) {\n                    Text(\"All Length\")\n                        .bold()\n                        .multilineTextAlignment(.center)\n                }\n                Text(store.allLength ?? \"\")\n                    .foregroundColor(.red)\n                    .font(.system(size: 12))\n                    .lineLimit(5)\n                \n                Button(action: {\n                    Task {\n                        await store.content.fetchContentValue()\n                    }\n                }) {\n                    Text(\"Fetch Content\")\n                        .bold()\n                        .multilineTextAlignment(.center)\n                }\n                Text(store.content.value ?? \"\")\n                    .foregroundColor(.red)\n                    .font(.system(size: 12))\n                    .lineLimit(5)\n            }\n        }\n        .padding(.horizontal, 100)\n    }\n}\n```\n\n\n## MIT License\n\nCopyright (c) 2021 Redux, ReactComponentKit\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freactcomponentkit%2Fredux","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Freactcomponentkit%2Fredux","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freactcomponentkit%2Fredux/lists"}