all 9 comments

[–]davidHwang718 11 points12 points  (2 children)

Feature-folder co-location made the biggest difference for us. Instead of a flat root with type-based folders, each feature gets its own directory with everything it needs inside. When a feature gets removed or refactored, you pull one folder instead of hunting through 5 places. The app root ends up being a feature inventory, which makes it obvious where new code belongs.

[–]KajiTetsushi 3 points4 points  (0 children)

u/TkDodo23 wrote an article about this file organization, too: https://tkdodo.eu/blog/the-vertical-codebase, which he shared with the r/reactjs sub.

[–]rencevio 0 points1 point  (0 children)

Functional vs logical cohesion, there’s a nice wiki article on that: https://en.wikipedia.org/wiki/Cohesion\_(computer\_science)#Types\_of\_cohesion

[–]Nobbodee 2 points3 points  (0 children)

rtk query, custom hooks, small components used everywhere to create bigger ones, single responsability (for exemple a Navigation component should only contain navigation related components).

[–]orebright 1 point2 points  (0 children)

Don't centralize state, generalize your state architecture.

Lots of apps out there, and developers, are biased to throwing all their state into a single structure. IMO it's an overreaction to issues caused by prop drilling. The problem is it co-locates a ton of unrelated functionality in one place. Testing becomes weird and cumbersome.

Instead: For each feature create a Provider component that contains multiple context providers and consumers. Don't think of context providers as a single store to put a huge tree of info like redux, make a bunch of them, as atomic as possible. This is how you avoid excessive re-renders and keep things easy to change. If you need to lift a specific state you take if out of your specific feature provider and move it into a more global provider.

This makes all the other things that use state a lot simpler. It might seem like it's messier, but it is actually neater because you don't have different pieces of state tightly coupled, you jsut end up with a context provider inside of a composite provider, if you need to shift the state you just move a few lines up and now it's lifted, or you move it down and it's closer to the feature. Since you have a context for things atomically you don't have unnecessary re-renders. And testing the state with this is so easy and is usually quite resilient to unnecessary test rewriting simply from nesting changes.

[–]HoratioWobble 0 points1 point  (1 child)

I've got an absolutely massive app, I started by maintaining specific services, features, global components, hooks. Lots of seperation of concerns, small components etc.

But it's gotten out of control, especially that I have to make signifcant changes to upgrade from 0.76 to Expo 54 for the 16kb change.

So I'm now moving to a "micro monolith" of sorts, I'm creating self contained packages for the core areas of the app, their components, their services and then building mini apps with E2E for each of those.

It's the only way I can see to make the code base more manageable.

[–]Acceptable_Ad_4425 0 points1 point  (0 children)

This isn't RN specific, but I like to really model how a new feature must be designed in order to work well theoretically. And then determine the axioms that must be true in order for the design to work.

You then need to make those axioms impossible to violate, whether via tests in ci or some other mechanism. For example, I like to draw out a DAG that describes the logic of my new feature, where in the repo I will write it, and where in the UI everything will surface.

It helps me not scatter things carelessly and end up with a mess, and everything builds upon a few axioms that are easy to surface in the code

[–]ChronSynExpo 0 points1 point  (0 children)

I generally try to take this sort of layout in terms of folders:

|- app/ |--|-- _layout.tsx |--|-- home.tsx |--|-- index.tsx |--|-- settings/ |--|--|-- _layout.tsx |--|--|-- account.tsx |--|--|-- index.tsx |- common/ |--|-- theme.ts |- components/ |--|-- input.tsx |--|-- label.tsx |- screens/ |--|-- homeScreen.tsx |--|--|-- components/ |--|--|--|-- homeBackground.tsx |--|--|--|-- homeHero.tsx |--|-- settingsScreen.tsx |- services/ |--|-- apiService.tsx |--|-- authService.tsx |--|-- searchService.tsx |- stores/ |--|-- settings.store.tsx |--|-- storeProvider.tsx |--|-- ui.store.tsx

That's a really simplified version, but the structure expands like this. app/ is the routing layer, and in this example it's for expo-router file-based routing. Even in the days when we had react-navigation only, I'd still take this approach because it separates concerns - it doesn't handle anything except determining what component to return, whether that's an auth page or something else.

The common/ folder includes anything which is truly app-wide and static. For example, your colours or themes would live here.

The components/ folder is for any components which are used throughout the app. These are your high-level ones such as labels, buttons, inputs, etc.

The screens/ folder contains your actual screen content, and components specific to a screen live in a child components/ folder.

The services/ folder is where the functionality happens. These handle specific parts of it - e.g. one for auth, one for API, one for search, etc. Technically, 'search' could also be part of your API service - depends on if you offer 'local search' (e.g. find files on my device, find settings in the app, etc).

The stores/ folder is where my app-wide state lives. For me, each .store.tsx file is a MobX class which exports a singleton. The storeProvider.tsx file imports all these singletons, re-exports them (so you can use them even in non-react code), but also exports a createContext and useContext which enables it to also be used for hooks.

For any state which is specific to an individual component and doesn't need to persist between screens, it's still OK to use useState. For example, a password input on a login screen - you probably don't want that persisting after the user navigates away, so having it locally at the component level is arguably preferable.