How do you organise TCA projects?

Title might be a bit vague and this topic is going to be very opinionated.

What’s the convention or how do people like to organises there TCA projects? By this I mean, if we look at the todos example from the original series introducing TCA. The AppState, AppAction, AppEnvironment and reducer were in the same file. Is that the convention?

Or is it better to have a separate file for each one e.g. in this instance there will be 4 different files with possible names:

  • TodoAppState
  • TodoAppAction
  • TodoAppEnvironment
  • TodoAppReducer

I know this is a personal preference kind of discussion but I’m curious how people are organising there projects. Especially the projects which are way more complicated.

There is no rule of thumb and you definitely wrote correctly — it's going to be opinionated. So I'll share mine:

We started using TCA from around 0.19 release or so. Before that we were using RxFeedback (somewhat similar library) and were organizing our code around faux-namespaces using enums. So basically under RxFeedback the code for a feature would look like this:

struct FeatureState {
  enum Action { }

  static func reduce(_ state: Self, action: Action) -> Self { }
}

After adopting TCA for new features we decided to slightly reorganize the structure by using faux-namespaces using enums, but still keep it in a single file:

enum Feature {
  struct State { }
  struct Environment { }
  enum Action { }
  
  static var reducer: Reducer<State,Action,Environment>
}

The only thing that mostly gets split out into separate files: production, preview and test environments.

1 Like

@Muhammed9991 One thing to consider is that the Reducer Protocol beta provides a more natural place to organize code: the reducer conformance. So your example above could be housed as:

struct Todos: ReducerProtocol {
  struct State { … }
  enum Action { … }
  /*
  There is no Environment. Todos holds directly onto dependencies,
  and you can use @Dependency for simple injection.
  */
  func reduce(
    into state: inout State, action: Action
  ) -> Effect<Action, Never> {
    …
  }
}

We consider the beta to be mostly stable, and plan on merging into a release in the coming weeks.

We personally have found it nice to have the entire domain above in a single file, to avoid hopping between files when working on a feature. We'll even sometimes include the view in the same file, though that's the first thing we'll break off into its own file when a feature becomes complex.

2 Likes

I like the idea of keeping all this logic in one file. Especially with the Reducer Protocol. I believe it will lead to a convention like you hinted @stephencelis. I like conventions lol.

A trivial thing in the grand scheme of things but one of the benefits I see in separating logic in different files is you can easily find the reducer, action etc for a given screen. In the case of the Reducer protocol. one file for all makes absolute sense. However, what would be a meaningful file name in this instance?

Another embedded opinionated question within an opinionated question :eyes:.

I'd probably name it after my conformance. Above that would be "Todos.swift" :slight_smile:

Simplicity. Noice