Skip to content

Commit

Permalink
Minor fixes and docs update (#98)
Browse files Browse the repository at this point in the history
* added more docs

* minor fix
  • Loading branch information
KazaiMazai authored Sep 10, 2024
1 parent b5872d2 commit 4a6503b
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 33 deletions.
121 changes: 121 additions & 0 deletions Sources/Puredux/Documentation.docc/Articles/ActionsDesignTips.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Actions Design Tips

Actions design examples and tips

## Overview

Puredux is very flexible in how actions are defined.

The examples below illustrate three different ways to define actions:

- Actions using a protocol
- Actions as an enum
- A combination of both

## Action using a protocol

Actions as a protocol can be defined the following way:

```swift
protocol Action {
// Define specific actions in your app by conforming to this protocol
}
```

Then, all app actions can be defined as structs conforming to the protocol:

```swift
struct IncrementCounter: Action {
// ...
}
```

In this case, the reducer would look like this:

```swift
mutating func reduce(_ action: Action) {
switch action {
case let action as IncrementCounter:
// ...
default:
break
}
}
```

### Pros

- Simple, yet flexible actions design
- Side effects with `AsyncAction`s

### Cons

- Addition overhead related to type casting of actions in reducers

## Action as an enum

Actions as an enum can be defined like this:

```swift
enum Action {
case incrementCounter
}
```

In this case, the reducer would look like this:

```swift
mutating func reduce(_ action: Action) {
switch action {
case .incrementCounter:
// ...
default:
break
}
}
```

### Pros

- Low-cost in terms of performance due to enum usage

### Cons

- No support for side effects with `AsyncAction`
- Scaling might be tricky in large applications

In larger apps, actions can be broken down into global and feature-specific actions:

```swift
enum AppActions {
case featureOne(FeatureOneActions)
case featureTwo(FeatureTwoActions)
}
```

## Action as an enum behind a protocol:

Taking the best of the two worlds:

```swift
protocol Action { }

enum FeatureOneActions: Action {
case incrementCounter
}
```

In this case, the reducer would look like this:

```swift
mutating func reduce(_ action: Action) {
switch action {
case let action as FeatureOneActions:
switch action {
// ...
}
default:
break
}
}
```
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Stores Tree Architecture
# Application Stores Architecture

Puredux allows you to build a hierarchical store tree. This architecture facilitates building applications where state can be shared or isolated, while state mutations remain predictable.

Expand All @@ -11,7 +11,50 @@ The store tree hierarchy ensures that business logic is completely decoupled fro

Make your app driven by business logic, not by the view hierarchy.

## Bulding the Hierarchy
## Choosing the Stores hierarchy

Puredux provides flexibility in structuring your app with the following store hierarchy options:

- Single Store
- Multiple Independent Stores
- Multiple Stores Tree

### Single Store

The single store pattern is best suited for very simple applications where the state management requirements are minimal. It consolidates the entire application state into one store.

However, this approach is not recommended when the app has multiple instances of the same module within the store.
In this context, a "module" refers to a feature or screen, represented by a pair consisting of a State and a Reducer. Managing multiple instances within a single store can become cumbersome.

### Multiple Independent Stores

This pattern is ideal when you have multiple modules that are completely independent of each other and do not require communication or shared state.
Each module has its own store, which simplifies state management for isolated components or features. The lack of dependencies between modules also ensures that changes in one module do not affect others, leading to a more modular and maintainable structure.

### Multiple stores tree

The multiple stores tree is the most flexible and scalable store architecture. It is designed to handle complex applications by organizing stores in a hierarchical structure. This approach offers several benefits:

- **Feature scope isolation**: Each feature or module has its own dedicated store, preventing cross-contamination of state.
- **Performance optimization**: Stores can be made pluggable and isolated, reducing the overhead of managing a large, monolithic state and improving performance.
- **Scalable application growth**: You can start with a simple structure and add more stores as your application grows, making this approach sustainable for long-term development.

#### For most applications, the recommended setup is

- A root store to manage shared, global application state
- Individual stores for each screen, ensuring clear separation of concerns

#### For larger applications or more complex use cases, this can evolve into

- A shared root store for common application-wide state
- Separate feature stores to handle specific functionality or domains
- Individual stores for each screen to manage their local state independently

This hierarchical approach allows your application to scale efficiently while maintaining a clean and organized state management strategy.

Here is a tree hierarchy example below.

## Bulding the Tree Hierarchy

**Step 1: Root Store**

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Documentation v1.x

From Documentation Archive

## Basics

- State is a type describing the whole application state or a part of it
Expand Down
36 changes: 28 additions & 8 deletions Sources/Puredux/Documentation.docc/Articles/GettingStarted.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Getting Started

Making your first steps with Puredux

## Installation

### Swift Package Manager
Expand Down Expand Up @@ -60,22 +62,35 @@ At its core, Puredux follows a predictable state management pattern that consist
+----------------+ +----+---+
```

## Store Definitions

Let's break down a typical store setup using Puredux.
## Actions Definitions

**1. Define the Action Protocol**:
Puredux is very flexible in how actions are defined.

Actions in Puredux follow a protocol that ensures they can be handled uniformly.
See more the examples here:
<doc:ActionsDesignTips>

Let's define an action as plain protocol:

```swift
protocol Action {
// Define specific actions in your app by conforming to this protocol
}
```
Then, all app actions can be defined as structs conforming to the protocol:

```swift
struct IncrementCounter: Action {
// ...
}
```

## Store Definitions

Let's break down a typical store setup using Puredux.

**2. Define the AppState**:

**1. Define the AppState**:

The application’s state can be represented by a struct, which will store the data relevant to your app.
The reduce method defines how the state will change in response to an action.
Expand All @@ -86,11 +101,16 @@ struct AppState {
// Define your app's state properties here

mutating func reduce(_ action: Action) {
// Logic for how the state should update when an action is dispatched
switch action {
case let action as IncrementCounter:
// ...
default:
break
}
}
}
```
**3. Define the Store and Inject it**:
**2. Define the Store and Inject it**:

Using the root AppState, we create a store that integrates actions and the state.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ Now there is no need in Action Interceptor any more, built-in `AsyncActions` pro
- DispatchQueue.main.async {
- action.execute(completeHandler: dispatch)
- }
- },
- reducer: reducer
- },
- reducer: reducer
-)

+ let store = StateStore(
Expand All @@ -67,18 +67,18 @@ Now there is no need in Action Interceptor any more, built-in `AsyncActions` pro

```diff
- let childStore: StateStore<(AppState, LocalState), Action> = storeFactory.childStore(
- initialState: LocalState(),
- initialState: LocalState(),
- reducer: { localState, action in
- localState.reduce(action: action)
- }
- localState.reduce(action: action)
- }
- )


+ let childStore: StateStore<(AppState, LocalState), Action> = rootStore.with(
+ LocalState(),
+ reducer: { localState, action in
+ localState.reduce(action: action)
+ }
+ LocalState(),
+ reducer: { localState, action in
localState.reduce(action: action)
+ }
+ )

```
Expand All @@ -105,13 +105,11 @@ Follow deprecation notices documentation for more details.
2. Inject root store with `@InjectEntry`:

```diff

+ extension Injected {
+ @InjectEntry var appState = StateStore<AppState, Action>(AppState()) { state, action in
+ state.reduce(action)
+ }
+ }

```

3. Wrap your `FancyView` in a container view with a`@State` store of a proper configuration
Expand Down Expand Up @@ -143,7 +141,7 @@ The Presentable API is still available, so only minor changes are needed.

+ myViewController.setPresenter(
+ store: store,
+ props: { state, store in
+ props: { state, store in
+ // ...
+ },
+ presentationQueue: .sharedPresentationQueue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ let store = StateStore(...)

It's important to note that previously, `store()` was mainly used to create a weak reference to avoid creating a reference cycle, which was a concern in the old API.

However, the new `eraseToAnyStore(`)` method works differently. It keeps a regular reference to the store, and the new API and internals ensures that reference cycles are not created by mistake.
However, the new `eraseToAnyStore()` method works differently. It keeps a regular reference to the store, and the new API and internals ensures that reference cycles are not created by mistake.

### Observer Changes

Expand Down
4 changes: 4 additions & 0 deletions Sources/Puredux/Documentation.docc/Articles/SideEffects.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,7 @@ let store = StateStore<AppState, Action>(AppState()) { state, action in

store.dispatch(.startJob)
```

A powerful feature of State-driven Side Effects is that their scope and lifetime are defined by the store they are connected to. This makes them especially beneficial in complex store hierarchies, such as app-level stores, feature-scoped stores, and per-screen stores, as their side effects automatically align with the lifecycle of each store.

Read more about stores hierarchy in: <doc:ApplicationStoresArchitecture>
2 changes: 1 addition & 1 deletion Sources/Puredux/Documentation.docc/Puredux.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ streamline state management with a focus on unidirectional data flow and separat
### Essentials

- <doc:GettingStarted>
- <doc:StoresTreeArchitecture>
- <doc:ApplicationStoresArchitecture>
- <doc:SideEffects>
- <doc:Performance>

Expand Down
6 changes: 4 additions & 2 deletions Sources/Puredux/Store/Core/AnyStoreObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,13 @@ final class AnyStoreObject<State, Action>: StoreObjectProtocol, Sendable where S

extension AnyStoreObject {
func map<T>(state transform: @Sendable @escaping (State) -> T) -> AnyStoreObject<T, Action> {
AnyStoreObject<T, Action>(boxed.map(transform: transform))
let store = boxed.map(transformState: transform)
return AnyStoreObject<T, Action>(store)
}

func map<A>(actions transform: @Sendable @escaping (A) -> Action) -> AnyStoreObject<State, A> {
AnyStoreObject<State, A>(boxed.map(actionsTransform: transform))
let store = boxed.map(transformActions: transform)
return AnyStoreObject<State, A>(store)
}
}

Expand Down
14 changes: 7 additions & 7 deletions Sources/Puredux/Store/Core/StoreObjectProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,23 @@ extension StoreObjectProtocol {
)
}

func map<T: Sendable>(transform: @Sendable @escaping (State) -> T) -> any StoreObjectProtocol<T, Action> {
func map<T: Sendable>(transformState: @Sendable @escaping (State) -> T) -> any StoreObjectProtocol<T, Action> {
StoreNode(
initialState: Void(),
stateMapping: { state, _ in transform(state) },
stateMapping: { state, _ in transformState(state) },
actionMapping: { $0 },
parentStore: self,
reducer: {_, _ in }
reducer: { _, _ in }
)
}

func map<A: Sendable>(actionsTransform: @Sendable @escaping (A) -> Action) -> any StoreObjectProtocol<State, A> {
StoreNode(
func map<A: Sendable>(transformActions: @Sendable @escaping (A) -> Action) -> any StoreObjectProtocol<State, A> {
StoreNode<Self, Void, State, A>(
initialState: Void(),
stateMapping: { state, _ in state },
actionMapping: actionsTransform,
actionMapping: transformActions,
parentStore: self,
reducer: {_, _ in }
reducer: { _, _ in }
)
}
}

0 comments on commit 4a6503b

Please sign in to comment.