Project Standards for High-Quality Code

When it comes to organizing React SPAs a great reference is:

It is a goldmine of great ideas to help you keep your codebase simple and clean (while also scaling well for very large applications).

Folder Structure

If you need a scalable folder structure for your React SPA, you should try this one from bulletproof-react. If you use a meta-framework, these conventions may still work fine. Having said that, there are some slightly unique things to be aware of when building with Next.js or Remix. Both frameworks have an approach for SSR (server-side rendering) and for avoiding network waterfalls, and thus will probably require some changes from the folder structure in bulletproof-react. The good news is since they recommend a folder structure you don’t have to think about it too much.

src
|
+-- assets            # assets folder can contain all the static files such as images, fonts, etc.
|
+-- components        # shared components used across the entire application
|
+-- config            # all the global configuration, env variables etc. get exported from here and used in the app
|
+-- features          # feature based modules

    /awesome-feature
    |
    +-- api         # exported API request declarations and api hooks related to a specific feature
    |
    +-- assets      # assets folder can contain all the static files for a specific feature
    |
    +-- components  # components scoped to a specific feature
    |
    +-- hooks       # hooks scoped to a specific feature
    |
    +-- routes      # route components for a specific feature pages
    |
    +-- stores      # state stores for a specific feature
    |
    +-- types       # TypeScript types for TS specific feature domain
    |
    +-- utils       # utility functions for a specific feature
    |
    +-- index.ts    # entry point for the feature, it should serve as the public API of the given feature and exports everything that should be used outside the feature

|
+-- hooks             # shared hooks used across the entire application
|
+-- lib               # re-exporting different libraries preconfigured for the application
|
+-- providers         # all of the application providers
|
+-- routes            # routes configuration
|
+-- stores            # global state stores
|
+-- test              # test utilities and mock server
|
+-- types             # base types used across the application
|
+-- utils             # shared utility functions

To avoid falling victim to analysis paralysis in this area: don't spend more than 5 minutes trying to plan a folder structure that will meet all of your future project needs. It's usually better to simply organize as you go. If you don't know what to do yet - just keep everything in the same flat folder (src). Once you get 10+ files in there that each has its own concern, you'll start to see the patterns that should dictate your folder structure.

If you're not a fan of the approach above, you might like this alternative folder structure by Josh Comeau instead. The article is a great read regardless if you end up adopting the recommendations, because it walks you through multiple scenarios for organizing your project.

Component Format

Writing Functional React Components

In general, you should think of the file contents in your React components as appearing in this order:

  1. Imports and constants
  2. Prop type definition
  3. Component State
  4. Other hooks
  5. Effects
  6. Helper functions (scoped)
  7. JSX (return statement)
  8. Abstracted JSX only used in this file

Here's a sample component to help you understand:

/**
 * 1. Imports and constants
 * You can configure an alias in your build tool that allow you to reference a root or specific folder
 * leads to cleaner imports and not nested spaghetti code like ../../../../some/random/folder/far/away
 */
import { blahblahblah } from '@/features/blah'
import { UserContext } from '@/contexts/user'

// for additional organization in the imports section ^^, you can
// alphebetize them with a package like @trivago/prettier-plugin-sort-imports

const operations = {
  '+': (left: number, right: number): number => left + right,
  '-': (left: number, right: number): number => left - right,
  '*': (left: number, right: number): number => left * right,
  '/': (left: number, right: number): number => left / right,
}

/**
 * 2. Prop type definition (or use interface, it doesn't really matter, just be consistent)
 */
type CalculatorProps = {
  left: number
  operator: keyof typeof operations
  right: number
}

/**
 * 1. Prefer simple function definition over const definition
 *    e.g. const Calculator: React.FC<Props> = () => {}
 * 
 * 2. Use React's Type system for your components that have *optional children
 *    Required children should be added to the props type definition
 *    e.g. function ComponentWithChildren(props: PropswithChildren<PropTypeDefinition>) {}
 */
export function Calculator({left, operator, right}: CalculatorProps) {
  /**
   * 3. Component State
   */
  const { user } = useContext(UserContext);
  const [someState, setSomeState] = useState();
  const [someOtherState, setSomeOtherState] = useState();
  const result = operations[operator](left, right)
  const someCondition = true;

  /**
   * 4. Other hooks
   */
  const cachedValue = useMemo(calculateValue, dependencies)
  const cachedFn = useCallback(fn, dependencies)

  /**
   * 5. Effects
   */
  useEffect(() => {
    // your effect code

    return () => {
      // your effect cleanup code
    }
  }, dependencies);

  /**
   * 6. Helper functions
   */
  function helperFunctionThatReliesOnComponentState() {
    cachedFn(cachedValue);
  }

  /**
   * JSX / Return statement
   * Advice: you can keep your return statements clean by moving static JSX to their own micro-components at the bottom of this file in step 8.
   */
  return (
    {someCondition ? <ShortenedMarkup /> : (
      <div>
        <code>
          {left} {operator} {right} = <output>{result}</output>
        </code>
      </div>
    )}
  );
}

/**
 * 8. Abstracted JSX used only in this file
 */
function ShortenedMarkup() {
  return (
    <div>
      <div>
        <div>
          <div>
            <div>
              Some UI content that doesn't rely on the props/state of the component above
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

Eslint and Prettier can also be very effective at enforcing project standards and consistency. Start with the examples provided in the bulletproof-react repo:

At a minimum, you should always have an eslint configuration in your project, but running both (eslint is more for catching code smells and potential bugs, and prettier is for opinionated formatting) is recommended.

Maintaining Documentation

Getting engineers to properly document their code can sometimes be difficult (and slow down team productivity!)

The question typically isn't "how can we convince our team and stakeholders that well-written documentation is worth it". The right question to ask is "how can we make writing and maintaining documentation easier for our developers". Everyone knows great docs are worth their weight in gold. The hard part is writing great docs.

  1. Use TypeScript - TypeScript can create all sorts of documentation that your IDE (like Visual Studio Code) can display to you while coding. Using TypeScript properly means that all the engineers on your team can:
    1. Analyze function signatures, parameters, and their return types without leaving the file you are working in
    2. Autocomplete while typing, IntelliSense, etc.
    3. Way more!
  2. Storybook for components and UI libraries - leveraging something like Storybook makes it easier for team members to explore the various states/functionality of your reusable components. It also makes it easier to create entire design systems that can be used across your organization
  3. Commenting systems like JSDoc - technology like this assumes you write plentiful documentation in comments next to your code, and those same comments can be parsed to generate actual HTML webpages that you can navigate, view, etc. It basically tries to take all of your comments throughout your codebase and generate swagger-like documentation for it. If you did BOTH of the above options (1 and 2) this may not be necessary, but if one or both of the options above isn't viable for you then this may be helpful.

You should at minimum be picking 1 option above. Or an approach that mixes all 3:

  • Using TypeScript
    • Prevents an entire class of bugs from arising, and gives IDE more capabilities (Intellisense, auto-complete, etc.)
  • Using Storybook for all atomic/reusable components
    • This would make it easy for engineers AND non-engineers peripheral to the project to browse the various components and their different states (centralized interactive design system)
  • Writing jsdoc-like comments above non-react functions that you define in your codebase
    • Write jsdoc comments on functions defined that are NOT react components or something that returns JSX (utility functions, functions that are business/domain logic, data-fetching, etc.)