Combining shared and local state

Does anyone know how to share some of the parent state with child state, in addition to having custom state properties in the child state, while maintaining propagation of any state changes? Thanks!

Example:

struct RootState: Equatable {
    var books: IdentifiedArrayOf<Book> = []

    var addBook: AddBookState {
        get {
            AddBookState(books: books)
            // ISSUE: `searchResults` custom state cannot be set here, thus getting lost
        }
        set {
        	self.books = newValue.books
        }
    }
}

struct AddBookState: Equatable {
	// shared state
	var books: IdentifiedArrayOf<Book>

	// custom, "AddBook" domain-specific state
	var searchResults: [String] = []
}

AddBookState can add a new book to the shared books state. However, it also needs an additional custom state, such as searchResults. Since the AddBookState is a computed property to ensure the propagation of changes made to books, I don't know how to maintain the values of the custom searchResults state property.

1 Like

"Local state" still needs to be brought back to the root state at some point, or it will indeed be lost. I'd put your searchResults 'state' in your RootState, then pass both down to your AddBookState in your getter and set them in your setter.

That's definitely an option, but having many properties such as searchResults would pollute the RootState domain. I'm trying to avoid that...

The idea, I believe, is to create “feature” states that encapsulate other states. All state has to flow top to bottom (and back), so if it’s missing at the top, it will never propagate down properly.

-Paul

As mentioned above, the only way to ensure that the child state data isn't lost is to capture it somewhere up in the state hierarchy.

If polluting the root state is a concern, maybe it's worth considering breaking that into its own state, especially if it's bound to grow:

struct SearchState: Equatable {
  var searchResults: [String] = []
  /* ... */
}

Hm, interesting. With that, I'm guessing you would pass the whole parent state into a particular view, and scope it down to appropriate parts of a state, as needed.

Something like this?

// MARK: Parent

struct ParentState: Equatable {
	var books: [Book] = []
	var search = SearchState()
}

enum ParentAction: Equatable {
	var search(SearchAction)
	...
}

// MARK: Search

struct SearchState: Equatable {
	var searchResults: [String] = []
	...
}

enum SearchAction: Equatable {
	...
}

// MARK: AddBookView

struct AddBookView: View {
	let store: Store<ParentState, ParentAction>

	var body: some View {
		WithViewStore(store.scope(state: { $0.search }, action: ParentAction.search)) { searchViewStore in
			// use `searchViewStore` to access search state, and send SearchAction actions

			WithViewStore(store.scope(state: { $0.books })) { booksViewStore in
				// use `booksViewStore` to access books state, and send ParentAction actions
			}
		}
	}
}

If AddBookView has it all needs between these two, then I don't see why this wouldn't work. I agree that having an AddBookState that is carbon-copy of the root state is redundant, but you know more about how each of these will potentially grow.

1 Like

Agree. Thanks!