How to share states between different levels of nested states?

I really like the architecture and the package, thanks a lot ! I am currently converting my old app architecture to CA but hitting some boundaries currently.

In my application the user can use a sheet to either create, edit or start time tracking tasks from different places. The three places are:

  • Show sheet in create mode from the list of all tasks
  • Show sheet in edit mode from a detail view of a task
  • Show sheet in start task mode from an subview anywhere in the app

My app has the following state tree ( simplified)

public struct AppState: Equatable {
    var overview: OverviewState = OverviewState()
    var runningTask: RunningTaskState = RunningTaskState()
}

public struct OverviewState: Equatable {
    var tasks: Loadable<[Task]> = .initial    
    var allProjects: Loadable<[Project]> = .initial
    var showCreateTaskView: Bool = false
}

public struct RunningTaskState: Equatable {
    var currentlyRunningTask: Loadable<Task?> = .initial
    var taskIsRunning: Bool = false
    var showStartTaskView: Bool = false
}

The sheet I want to display needs access to the allProjects property of the OverviewState . So the question of mine would be, how can I access the allProjects property of the OverviewState from the RunningTaskState, despite the two states being on the same level and do not know each other ?

Edit:

Or would it be better to have an additional state below the global level which handles the sheet state since it can be accessed from several places ?

There are many ways to coordinate and normalize state, but perhaps the most straightforward, very-boilerplatey-but-safe example is by holding onto allProjects on AppState and projecting it into OverviewState and RunningTaskState:

/// Container for shared projects state and task-specific state.
public struct OverviewProjectsState: Equatable {
    var allProjects: Loadable<[Project]>
    var overview: OverviewState
}

/// Container for shared projects state and overview-specific state.
public struct RunningTaskProjectsState: Equatable {
    var allProjects: Loadable<[Project]>
    var runningTask: RunningTaskState
}

public struct AppState: Equatable {
    var overview: OverviewState = OverviewState()
    var runningTask: RunningTaskState = RunningTaskState()
    var allProjects: Loadable<[Project]> = .initial

    var overviewWithProjects: OverviewProjectsState {
        get {
            RunningTaskProjectsState(
                allProjects: self.allProjects,
                runningTask: self.runningTask
            )
        }
        set {
            self.allProjects = newValue.allProjects
            self.runningTask = newValue.runningTask
        }
    }
    var runningTaskWithProjects: RunningTaskProjectsState {
        get {
            RunningTaskProjectsState(
                allProjects: self.allProjects,
                runningTask: self.runningTask
            )
        }
        set {
            self.allProjects = newValue.allProjects
            self.runningTask = newValue.runningTask
        }
    }
}

This comes with a lot of boilerplate and nesting, but is safer than holding two instances of the same state and attempting to synchronize them.

2 Likes

Thanks a lot for the proposed solution! - I will try that and see if it will work out in the end. Currently I find it kinda hard to decide where to put what in the state and see my views become tightly coupled to the state. But I think this is just because the whole architecture is new.

Thanks for sharing Stephen, this helps a lot. In addition to the proposed way, would you mind sharing other approaches in situations like this?

I've personally been using TCA in production for almost 3 months now, and, in my experience, the trickiest thing is striking a good balance between simplicity, reusability, and verbosity of a chosen state structure.

I imagine I'm not alone in this, so talking a bit more about state structuring, especially in complex projects, might be beneficial to many people.

In case that's something you'd want to explore, I'd be happy to share an outline of my complex project, for us to use as a discussion point about state structuring.

Anyway, thanks for everything you do.

1 Like

I would like to say that I agree with Damir. It would be really cool to get more insights in how to structure state in a more complex app.

Also a huge thanks by me. I just started with using TCA but my app is already looking more structured and easier to understand. Thanks for all of your work and all of the videos you are creating.

2 Likes

How about using a backing property to make the code a bit simpler? I'm not sure at all about this approach, because I guess it'll result in having multiple copies of the state, but the code is a little nicer, at least in my opinion.

public struct OverviewState: Equatable {
    var tasks: Loadable<[Task]> = .initial
    var showCreateTaskView: Bool = false
    var allProjects: Loadable<[Project]>
}

public struct RunningTaskState: Equatable {
    var currentlyRunningTask: Loadable<Task?> = .initial
    var taskIsRunning: Bool = false
    var showStartTaskView: Bool = false
    var allProjects: Loadable<[Project]>
}

public struct AppState: Equatable {
    var allProjects: Loadable<[Project]> = .initial

    var _overviewState: OverviewState?
    var overviewState: OverviewState {
        get {
            if _overviewState == nil {
                return OverviewState(allProjects: allProjects)
            }


            var copy = _overviewState!
            copy.allProjects = allProjects
            return copy
        }
        set {
            _overviewState = newValue
            allProjects = newValue.allProjects
        }
    }

    var _runningTaskState: RunningTaskState?
    var runningTaskState: RunningTaskState {
        get {
            if _runningTaskState == nil {
                return RunningTaskState(allProjects: allProjects)
            }


            var copy = _runningTaskState!
            copy.allProjects = allProjects
            return copy
        }
        set {
            _runningTaskState = newValue
            allProjects = newValue.allProjects
        }
    }
}

I know this is too late now but wanted to share my experience in case it helps someone else since this seems to be one of the top posts when searching for the topic.
I have a running production app for almost two years and the initial approach we followed was using computed properties. That comes with a lot of boiler plate code, it’s counter intuitive to explain to someone who is joining the project, harder to understand how information flows, difficulty in debugging increases and more error prone to sync state. While you can fix some of these using some meta programming tool such as Sourcery, that also introduces some friction to onboard new people to the project. Again for me it’s very important aspect of any architecture chosen the ability to onboard new people and let them feel comfortable quickly so they car start producing quality code in the less amount of time possible.
We are trying to move away from all of that by moving this portions that need to be shared to the environment and each feature observe/react to them independently. That’s real SoT and so far has been working great. So in this case you would move all projects to your environment. Make a storage class holding a published property to it and expose it through the environment so whatever feature needs it can simply observe/react to its changes.

2 Likes

How do you react to changes in the environment?
I am currently not sure how to do this because TCA only observes the store to render it's UI.