Opinionated Project Structures in React.js without a Framework

localpath

Garrick Crouch

Posted on October 8, 2021

Opinionated Project Structures in React.js without a Framework

After using React professionally for nearly two years, I've finally settled on sensible ways to structure or compose applications in React. This strictly applies to using "vanilla" React, not any sort of framework, ecosystem, or starter environment. I say all that to say this, there's a million ways to structure a react app, and none of them are wrong on their face, but do they really make sense?

design systems shouldn't dictate project structure, focus on composability both in your system architecture and components

I've spent lots of time studying different ways to structure a react app and they often felt disjointed or opinionated in ways that I think a person would have a hard time justifying or explaining why it's structured that way. At the point of working on 10+ react apps, I finally asked myself, "why do we structure these apps the way we do? and can I give at least one reason for every folder or file being where it is that isn't arbitrary."

Studying different methods of application structure not just revolving around react but software in general, I finally decided to plunge into refactoring applications at work and being able to say, "this is why these folders are where they are, and this is why I think it's a good idea."

I settled on a project structure that tries to embody the word "responsibility". I realized that in a UI library, it's very easy to cram everything into messy hierarchies or paths. Basically, this thing is inside this thing is inside this thing and so on.

One final note before laying out the structure and the why for, testing is crucial and is the only first class file, meaning it can live where you want it to based on needs, since many people need to test various parts of their app in widely different ways since unit, feature, integration, and browser testing can be a wild place to wander.

The Structure - We Make Assumptions

src/
__tests__/
-common/
-components/
-context/
-forms/
-hooks/
-layouts/
-pages/
-routes/
-services/
-tables/
-utils/
--constants.jsx
--helpers/
-App.jsx
Enter fullscreen mode Exit fullscreen mode

I'll step through each directory and explain the thought behind its placement and its "responsibility". This is after all, entirely about inferring structure based on responsibility.

  1. src/ - just simply what contains the app in total.

  2. tests - would contain feature and integration tests for me but may also contain your unit tests if that is what the team needs or likes (no opinion beyond you need testing)

  3. common - this houses any component that is used more than once in the app with the potential of living in your own design system or external library. (excludes other components we'll go over). Be cautious about abstracting too early because refactoring is very easy using this structure. Considering that everything has its "place", we need good reason to say this component is common to the app yet doesn't have behavior which determines it to belong in another directory.

  4. components - houses one offs or atoms, as well as wrapper, or container components. These will usually have business logic, hoisted state, and provide a place that atoms are composed and used with each other.

  5. context - any context providers. these contain your global state, extended stores, and dependency injection components.

  6. forms - houses any form element component in full. Forms make up the majority of data driven applications so its important to either reuse them in smart ways or keep track of their namespaces.

  7. hooks - any custom hooks you may need to wrap the native react lifecycle hooks.

  8. layouts - any structured or skeleton like html templates. Think navs, footers, sidebars, dashboard wrappers, content containers. Usually encapsulates exactly what the name implies, the page layout of your app.

  9. pages - any component that is routed to. very simple concept to apply and maintain. if it is a routed component, it is a page. Then you take it from there with how you compose pages inside layouts or vice versa. Match the page name to the route which should match the URL. It's a document location so keep it simple and absolutely consistent. /dashboard would be Dashboard, /users/:id would be User, /users would be Users, etc. Follow namespacing rules for nested URL structure. /users/:id/profile would be UserProfile.

  10. routes - route declaration files, protected and role based routing. These routes will only ever point to a page. Depending on what router you use, determines how you compose or use the route definitions.

  11. services - links to the outside world via http, ws, and any 3rd party integrations. Defines the network clients.

  12. tables - the other half of data driven applications are tables. namespaced and composed of only ever a table element integrating other components as needed from common/components.

  13. utils - contains a constants page, and a helpers dir. constants are meant to be used throughout the app whether environmental or domain driven. The helpers dir is non react helper functions.

Individual Component Hierarchy

components/
YourComponent/
--index.jsx
--test.jsx
Enter fullscreen mode Exit fullscreen mode

The rule to follow is that every component only ever contains an index.jsx file that exports itself as the default named function. This allows for extremely easy colocation of unit tests per component and if you need it, styles as well. I personally avoid CSS in JS due to responsibility, but you may favor that as a development habit or your build tool may necessitate it, though I prefer to be unbound by structuring my app based on the build tool.

Be cautious about deeply nesting inside components, when you find yourself nesting too much take the time to think about how you could compose it to not nest at all or would it serve better at being pulled into atoms.

Takeaway

Take the time to understand why code locates where it does and it'll become second nature to build systems that follow structured approaches. Reducing cognitive overhead is crucial to being effective in software. Complexity in structure or the lack of structure does not help. Focus on composability based on responsibility.

With a structure like this it is extremely easy to refactor an application when you see code bloat or code rot. Focusing on responsibility means you reduce spaghetti since component dependencies are decoupled more and more.

💖 💪 🙅 🚩
localpath
Garrick Crouch

Posted on October 8, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related