React + TypeScript Style Guide

A clean and consistent guide for writing high-quality React + TypeScript code.

β€œAny fool can write code that a computer can understand. Good programmers write code that humans can understand.”
β€” Martin Fowler

πŸš€ React + TypeScript Style Guide

A structured, scalable, and opinionated style guide for building maintainable React applications with TypeScript. This guide ensures consistency, clarity, and best practices across projects.

πŸ“– Table of Contents


🧠 Philosophy

This style guide is designed to ensure consistency, readability, and maintainability in React + TypeScript projects. By following a structured approach, we aim to reduce cognitive load, improve collaboration, and make codebases easier to scale.

πŸ”Ή Core Principles

βœ… Summary

By following this guide, teams can write cleaner, more scalable, and easier-to-maintain code. The focus is on consistency, clarity, and minimal cognitive load while following modern React + TypeScript best practices.


πŸ“‚ Folder Structure

A structured, feature-based folder organization ensures scalability, maintainability, and readability. This structure keeps related files encapsulated while providing clear separation between shared logic and feature-specific implementations.

πŸ”Ή General Folder Structure


πŸ”Ή Barrel Files & Wildcard Imports

Using barrel files (index.ts) can simplify imports and improve readability, but they should be used with caution. Overuse can lead to unintended re-exports, circular dependencies, and performance issues in certain frameworks like Remix.

When to Use Barrel Files

When to Avoid Barrel Files

Best Practices

❌ Avoid wildcard (*) imports as they increase bundle size, prevent effective tree-shaking, and can introduce unnecessary dependencies.

import * as utils from β€˜common/utils’

βœ… Prefer named imports for clarity and tree-shaking

import { formatDate, getUserProfile } from β€˜common/utils’

app/
β”œβ”€β”€ routes/
β”œβ”€β”€ src/
β”œβ”€β”€ assets/
β”‚   β”œβ”€β”€ fonts/
β”‚   β”‚   β”œβ”€β”€ roboto-bold.woff
β”‚   β”‚   β”œβ”€β”€ roboto-bold.woff2
β”œβ”€β”€ common/
β”‚   β”œβ”€β”€ components/
β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”œβ”€β”€ index.ts
β”‚   β”‚   β”œβ”€β”€ useFlag.ts
β”œβ”€β”€ config/                      # External service integrations only
β”‚   β”œβ”€β”€ analytics/
β”‚   β”‚   β”œβ”€β”€ index.ts
β”‚   β”‚   β”œβ”€β”€ useLucencyNumber.ts
β”‚   β”œβ”€β”€ apollo/
β”‚   β”‚   β”œβ”€β”€ ApolloProvider.tsx
β”‚   β”‚   β”œβ”€β”€ index.ts
β”œβ”€β”€ constants/                    # Global constants/utils (if used in multiple features)
β”‚   β”œβ”€β”€ guideUtils.ts             # Example: Used in multiple features
β”‚   β”œβ”€β”€ index.ts
β”‚   β”œβ”€β”€ user.ts
β”œβ”€β”€ pages/
β”‚   β”œβ”€β”€ guide/
β”‚   β”‚   β”œβ”€β”€ __tests__/
β”‚   β”‚   β”‚   β”œβ”€β”€ __mocks__/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ index.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ guideMock.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ Guide.test.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ guideUtils.test.ts
β”‚   β”‚   β”œβ”€β”€ common/
β”‚   β”‚   β”‚   β”œβ”€β”€ __tests__/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ GuideBadge.test.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ GuideHero/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ __tests__/
β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ GuideHero.test.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ GuideHero.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ GuideHeroLoading.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ index.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ GuideBadge.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ GuideLoading.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ index.ts
β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”‚   β”œβ”€β”€ index.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ useCreateGuideMutation.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ useGetGuideQuery.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ useUpdateGuideMutation.ts
β”‚   β”‚   β”œβ”€β”€ Guide.tsx
β”‚   β”‚   β”œβ”€β”€ index.ts               # For cleaner imports
β”‚   β”‚   β”œβ”€β”€ guideConstants.ts (if needed)
β”‚   β”‚   β”œβ”€β”€ guideUtils.ts (if needed)
β”‚   β”‚   β”œβ”€β”€ types.ts (if needed)
β”‚   β”œβ”€β”€ profile/
β”‚   β”‚   β”œβ”€β”€ __tests__/
β”‚   β”‚   β”‚   β”œβ”€β”€ __mocks__/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ index.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ profileMock.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ Profile.test.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ profileUtils.test.ts
β”‚   β”‚   β”œβ”€β”€ common/
β”‚   β”‚   β”‚   β”œβ”€β”€ __tests__/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ ProfileHero.test.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ ProfileHero/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ ProfileHero.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ ProfileHeroLoading.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ index.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ ProfileLoading.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ ProfileSidebar/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ ProfileSidebar.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ ProfileSidebarLoading.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ index.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ index.ts
β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”‚   β”œβ”€β”€ index.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ useCreateProfileMutation.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ useGetProfileQuery.ts
β”‚   β”‚   β”œβ”€β”€ Profile.tsx
β”‚   β”‚   β”œβ”€β”€ index.ts               # For cleaner imports
β”‚   β”‚   β”œβ”€β”€ profileConstants.ts (if needed)
β”‚   β”‚   β”œβ”€β”€ profileUtils.ts (if needed)
β”‚   β”‚   β”œβ”€β”€ types.ts (if needed)

πŸ”Ή Why This Works


🎭 Component Structure

A well-structured React component improves readability, maintainability, and consistency. This section defines how components should be structured, including ordering hooks, variables, functions, and the return statement.

πŸ”Ή General Rules for Components


πŸ”Ή Component Order

Components should follow this order:


βœ… Example: Standard Component Structure

export const Profile = () => {
  const navigate = useNavigate()
  const { accountHandle } = useParams()
  const { hasError, isLoading, profileData } = useGetProfileQuery(accountHandle)
  const [searchParams] = useSearchParams()
  const { id, image } = profileData ?? {}

  useEffect(() => {
    // Example: Track analytics
  }, [])

  const getProfileAvatar = () => {}

  const getProfileName = () => {}

  if (isLoading || isEmpty(profileData)) return <ProfileLoading />

  if (hasError) return <ProfileEmpty />

  return (
    <section>
      <ProfileHero />
      <div>
        <ProfileSidebar />
        <ProfileContent />
      </div>
    </section>
  )
}

πŸ”Ή Return Statement Spacing

βœ… Example:

export const Profile = () => {
  const { hasError, isLoading, profileData } = useGetProfileQuery()

  if (isLoading || isEmpty(profileData)) return <ProfileLoading />

  if (hasError) return <ProfileEmpty />

  return (
    <section>
      <ProfileHero />
      <div>
        <ProfileSidebar />
        <ProfileContent />
      </div>
    </section>
  )
}

❌ Avoid cramming return right after logic without spacing.

export const Profile = () => {
  const { hasError, isLoading, profileData } = useGetProfileQuery()
  if (isLoading || isEmpty(profileData)) return <ProfileLoading />
  if (hasError) return <ProfileEmpty />
  return (
    <section>
      <ProfileHero />
      <div>
        <ProfileSidebar />
        <ProfileContent />
      </div>
    </section>
  )
}
export const Profile = () => {
  const { hasError, isLoading, profileData } = useGetProfileQuery()
  return (
    <section>
      <ProfileHero />
      <div>
        <ProfileSidebar />
        <ProfileContent />
      </div>
    </section>
  )
}

πŸ”Ή Return Formatting in Functional Components

When returning JSX in functional components, maintain consistent spacing for clarity and readability.

βœ… General Rules:


βœ… Example: Correct Formatting

export const Profile = () => {
  const { hasError, isLoading, profileData } = useGetProfileQuery()

  if (isLoading) return <ProfileLoading />

  if (hasError) return <ProfileError />

  return (
    <section>
      <ProfileHero />
      <ProfileContent />
    </section>
  )
}

❌ Example: Incorrect Formatting

export const Profile = () => {
  const { hasError, isLoading, profileData } = useGetProfileQuery()

  if (isLoading) {
    return <ProfileLoading />
  }

  if (hasError) {
    return <ProfileEmpty />
  }

  return (
    <section>
      <ProfileHero />
      <ProfileContent />
    </section>
  )
}

βœ… Summary


πŸ”Ή Early Returns for Simplicity

To improve readability and reduce indentation, always return early in conditionals rather than nesting them inside larger blocks.

βœ… Example: Using Early Return for Cleaner Code

export const Profile = () => {
  const { hasError, isLoading, profileData } = useGetProfileQuery()

  if (isLoading) return <ProfileLoading />

  if (hasError) return <ProfileEmpty />

  return (
    <section>
      <ProfileHero />
      <ProfileContent />
    </section>
  )
}

❌ Example: Nested Conditionals (Harder to Read)

export const Profile = () => {
  const { hasError, isLoading, profileData } = useGetProfileQuery()

  if (isLoading) {
    return <ProfileLoading />
  } else {
    if (hasError) {
      return <ProfileEmpty />
    } else {
      return (
        <section>
          <ProfileHero />
          <ProfileContent />
        </section>
      )
    }
  }
}

πŸ”Ή JSX Formatting Rules

export const Profile = () => <section>...</section>
export const Profile = () => (
  <section>
    <ProfileHero />
    <ProfileSidebar />
  </section>
)

πŸ”Ή Function & Hook Spacing Rules

const navigate = useNavigate()
const { accountHandle } = useParams()
const { hasError, isLoading, profileData } = useGetProfileQuery(accountHandle)
const [searchParams] = useSearchParams()
const { id, image } = profileData ?? {}
const getProfileAvatar = () => {}

const getProfileName = () => {}
const navigate = useNavigate()
const { accountHandle } = useParams()

useEffect(() => {
  // Example: Sync data on mount
}, [])

πŸ”Ή Component Naming Conventions

export const ProfileHero = () => <div>Profile Hero</div>
const getProfileName = () => {}

πŸ”Ή Loading & Empty State Components

export const Profile = () => (
  <section className='bg-red'>
    <ProfileHero />
    <div>
      <ProfileSidebar />
      <ProfileContent />
      <Button>Click me</Button>
    </div>
  </section>
)
export const ProfileLoading = () => (
  <section className='bg-red'>
    <ProfileHeroLoading />
    <div>
      <ProfileSidebarLoading />
      <ProfileContentLoading />
      <div className='h-12 w-20'>
        <Skeleton variant='rounded' />
      </div>
    </div>
  </section>
)

πŸ”Ή When to Split Components?

A component should be split into smaller components if:


πŸ”Ή When to Use a common/ Component?

βœ… Example (Feature-Specific Component)

pages/profile/common/ProfileHero.tsx

βœ… Example (Shared Component)

common/components/ImageWithFallback.tsx

πŸ”Ή Why This Works


⚑ Functions & Utilities

This section defines where and how utility functions should be structured to ensure readability and maintainability.


πŸ”Ή Utility Function Placement

βœ… Example: Utility Function Placement

pages/profile/profileUtils.ts # Feature-specific utilities
constants/userUtils.ts # Shared utilities across features

βœ… Example: Exporting Multiple Utilities

const getProfileAvatar = () => {}

const getProfileName = () => {}

export { getProfileAvatar, getProfileName }

πŸ”Ή Formatting Rules for Functions

❌ Bad Example (Unnecessary Nesting)

const getUserDetails = user => {
  if (user) {
    return {
      id: user.id,
      name: user.name,
      email: user.email,
    }
  } else {
    return null
  }
}

βœ… Good Example (Flat and Readable)

const getUserDetails = user => {
  if (!user) return null

  return {
    id: user.id,
    name: user.name,
    email: user.email,
  }
}

πŸ”Ή Return Placement in Functions

Return statements inside functions follow consistent spacing rules for readability.

βœ… General Rules


βœ… Example: Early return at the start of the function (no blank line)

const getProfileRole = (profileData: ProfileData) => {
  if (!profileData?.id) {
    console.warn('Profile data is missing ID')
    return 'Guest'
  }

  return profileData.role
}

βœ… Example: Single-line early return (no extra space needed)

const getProfileRole = (profileData: ProfileData) => {
  if (!profileData?.id) return 'Guest'

  return profileData.role
}

βœ… Example: Returning directly in a function with no logic

const getProfileName = (profileData: ProfileData) => `${profileData.firstName} ${profileData.lastName}`

βœ… Example: if appears in the middle of the function (needs a blank line before it)

const getProfileName = (profileData: ProfileData) => {
  const { firstName, lastName } = profileData ?? {}

  if (!firstName || !lastName) return 'Guest'

  return `${firstName} ${lastName}`
}

❌ Example: Missing space before if when it’s in the middle of the function

const getProfileName = (profileData: ProfileData) => {
  const { firstName, lastName } = profileData ?? {}
  if (!firstName || !lastName) return 'Guest'

  return `${firstName} ${lastName}`
}

❌ Example: Extra blank line before a return when it’s the only statement

const getProfileName = (profileData: ProfileData) => {

  return `${profileData.firstName} ${profileData.lastName}`
}

❌ Example: Extra blank line before an early return at the start of a function

const getProfileName = (profileData: ProfileData) => {

  if (!firstName || !lastName) return 'Guest'

  return `${firstName} ${lastName}`
}

❌ Example: Single-line early return should stay inline

const getProfileRole = (profileData: ProfileData) => {
  if (!profileData?.id) { return 'Guest' }
}
const getProfileRole = (profileData: ProfileData) => {
  if (!profileData?.id) {
    return 'Guest'
  }
}

πŸ”Ή Summary of Return Placement Rules

CaseBlank Line Before Return?
Single return as the only function statement❌ No
Early return at the start of a function❌ No
if appears in the middle of the functionβœ… Yes
Final return in a function❌ No
Return inside a multi-line if blockβœ… Yes

πŸ”₯ Final Thoughts

Return placement follows the same logic as variables:

By keeping returns structured and predictable, code stays clean, readable, and consistent across the project. πŸš€


πŸ”Ή Why This Works


πŸ“‘ GraphQL Queries

A structured approach to handling GraphQL queries and mutations ensures readability, maintainability, and consistency across the application.

πŸ”Ή General Rules for GraphQL Queries & Mutations

βœ… Example:

src/pages/profile/hooks/useGetProfileQuery.ts # Feature-specific query
src/pages/profile/hooks/useCreateProfileMutation.ts # Feature-specific mutation
src/hooks/useGetPredefinedGuideTagsQuery.ts # Sitewide query (used across features)

βœ… Example:

query GetProfileQueryInProfile($id: ID!) { ... }

πŸ”Ή Feature-Based Queries vs. Sitewide Queries

To differentiate feature-specific GraphQL queries/mutations from global queries, we use a structured naming convention:

Feature-Based Queries & Mutations

βœ… Example:

src/pages/profile/hooks/useGetProfileQuery.ts # Query used only in Profile
src/pages/profile/hooks/useUpdateProfileMutation.ts # Mutation used only in Profile

βœ… Query Example:

query GetProfileQueryInProfile($id: ID!) {
  node(id: $id) {
    ... on Profile {
      id
      accountHandle
      displayName
      image
    }
  }
}

Sitewide Queries & Mutations

βœ… Example:

src/hooks/useGetPredefinedGuideTagsQuery.ts # Sitewide query

βœ… Query Example:

query GetPredefinedGuideTags {
  predefinedGuideTags {
    id
    name
  }
}

πŸ”Ή Why This Naming Works

πŸ“Œ Key Takeaways:


πŸ”Ή Example: Query for Fetching Profile Data

import { gql, useQuery } from '@apollo/client'

type UseGetProfileQueryResult = {
  hasError: ApolloError
  isLoading: boolean
  profileData: Extract<
    GetProfileQueryInProfileQuery['node'],
    {
      __typename?: 'Profile'
    }
  >
}

const profileQuery = gql(`
  query GetProfileQueryInProfile($id: ID!) {
    node (id: $id) {
      ... on Profile {
        id
        accountHandle
        displayName
        image
      }
    }
  }
`)

export const useGetProfileQuery = (id: string): UseGetProfileQueryResult => {
  const {
    data,
    error: hasError,
    loading: isLoading,
  } = useQuery(profileQuery, {
    variables: {
      id,
    },
  })

  return {
    hasError,
    isLoading,
    profileData: data?.node,
  }
}

πŸ”Ή Why This Works


πŸ”Ή Example: Mutation for Updating Profile

import { gql, useMutation } from '@apollo/client'

const updateProfileMutation = gql(`
  mutation UpdateProfileMutationInProfile($updateProfileInput: UpdateProfileInput!) {
    updateProfile(updateProfileInput: $updateProfileInput) {
      id
      displayName
    }
  }
`)

export const useUpdateProfileMutation = () => useMutation(updateProfileMutation)
export const ProfileForm = () => {
  const [updateProfile, updateProfileResult] = useUpdateProfileMutation()

  const onSubmit = async (id: string, displayName: string) => {
    try {
      await updateProfile({
        variables: {
          updateProfileInput: {
            displayName,
            id,
          },
        },
      })
    } catch (error) {
      console.error('Failed to update profile', error)
    }
  }

  return (
    <form onSubmit={onSubmit}>
      <button type='submit'>Update Profile</button>
    </form>
  )
}

πŸ”Ή Why This Works


🚩 Feature Flags

Feature flags enable us to conditionally enable or disable features without deploying new code. This approach allows for progressive rollouts, A/B testing, and safe feature releases.

πŸ”Ή General Structure

Feature flags are managed using two primary components:

  1. Feature Flags Configuration (featureFlags.ts)

    • This file defines all available feature flags.
    • Flags are stored as a record of boolean values.
  2. Feature Flag Hook (useFlag.ts)

    • A custom hook to read feature flag values.
    • Uses local storage overrides, allowing developers to toggle features locally.

πŸ“‚ File Structure

src/
β”œβ”€β”€ config/
β”‚   β”œβ”€β”€ feature-flags/
β”‚   β”‚   β”œβ”€β”€ featureFlags.ts    # Central feature flag configuration
β”œβ”€β”€ common/
β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”œβ”€β”€ useFlag.ts         # Hook to check feature flag status

πŸ”Ή Feature Flags Configuration

Feature flags are centrally defined in src/config/feature-flags/featureFlags.ts. This ensures all available flags are explicitly listed.

βœ… Example: Defining Feature Flags

// src/config/feature-flags/featureFlags.ts
type FeatureFlagNames = 'profileHeroV2' | 'profileV2'

const featureFlags: Record<FeatureFlagNames, boolean> = {
  profileHeroV2: false,
  profileV2: false,
}

export type { FeatureFlagNames }
export { featureFlags }

πŸ”Ή Accessing Feature Flags with useFlag

The useFlag hook retrieves the current state of a feature flag, checking for local storage overrides.

βœ… Example: Feature Flag Hook

// src/common/hooks/useFlag.ts
import { useState, useEffect } from 'react'
import type { FeatureFlagNames } from 'src/config/feature-flags/featureFlags'
import { useLocalStorageFlags } from './useLocalStorageFlags'

export const useFlag = (flagKey: FeatureFlagNames | string): boolean => {
  const [isFlagEnabled, setIsFlagEnabled] = useState(false)
  const [localFlags] = useLocalStorageFlags()

  useEffect(() => {
    if (flagKey in localFlags) {
      const { [flagKey]: localStorageFlag } = localFlags
      setIsFlagEnabled(String(localStorageFlag).toLowerCase() === 'true')
    }
  }, [flagKey, localFlags])

  return isFlagEnabled
}

πŸ”Ή Using Feature Flags in Components

βœ… Example: Conditionally Rendering Components

Feature flags allow conditional rendering of components within a section.

import { useFlag } from 'src/common/hooks/useFlag'
import { ProfileHero } from './ProfileHero'
import { ProfileHeroOld } from './ProfileHeroOld'

export const Profile = () => {
  const isProfileHeroV2Enabled = useFlag('profileHeroV2')

  return (
    <section>
      {isProfileHeroV2Enabled ? <ProfileHero /> : <ProfileHeroOld />}
    </section>
  )
}

πŸ”Ή Using Feature Flags for Route-Based Feature Toggles

For larger changes, such as enabling an entirely new Profile redesign, we rename the existing feature folder (profile) to profile-old and introduce a new profile/ folder.

Then, in PageRoutes.tsx, we dynamically choose which version of Profile to render based on the feature flag.

βœ… Example: Routing Feature Flag Usage

import { useFlag } from 'src/common/hooks/useFlag'
import { Routes, Route } from 'react-router-dom'
import { Home } from 'src/pages/home'
import { Profile } from 'src/pages/profile'
import { ProfileOld } from 'src/pages/profile-old'

export const PageRoutes = () => {
  const isProfileV2Enabled = useFlag('profileV2')

  return (
    <ScrollToTop>
      <Routes>
        <Route element={<Home />} path='/' />
        <Route
          element={isProfileV2Enabled ? <Profile /> : <ProfileOld />}
          path='/profile/:accountHandle'
        />
      </Routes>
    </ScrollToTop>
  )
}

πŸ”Ή Feature Flag Guidelines


βœ… Summary


πŸ”  Types & Interfaces

A consistent approach to defining types and interfaces ensures clarity, maintainability, and flexibility across the codebase.


πŸ”Ή General Rules


πŸ”Ή Component Props: Use interface

βœ… Example: Functional Component Props

interface ProfileHeroProps {
  onClick: () => void
  title: string
}

export const ProfileHero = ({ onClick, title }: ProfileHeroProps) => (
  <div onClick={onClick}>{title}</div>
)

βœ… Example: Extending an Interface

Use interface to extend props cleanly, while type uses & for merging multiple types.

import { Button } from '@travelpass/design-system'
import type { GenericAddress } from 'src/__generated__/graphql'

interface ProfileAddressProps extends GenericAddress {
  onClick: VoidFunction
}

export const ProfileAddress = ({
  addressLine1,
  city,
  country,
  onClick,
}: ProfileAddressProps) => (
  <section>
    <h2>{name}</h2>
    <p>{getAddress(addressLine1, city, country)}</p>
    <Button onClick={onClick}>Edit</Button>
  </section>
)

πŸ”Ή Utility Types: Use type

Use Pick<> when selecting only specific properties from a type, and Omit<> when removing specific properties.

These help create lightweight, flexible types for better reusability.

βœ… Example: Utility Type for Query Results

type UseGetProfileQueryResult = {
  hasError: ApolloError
  isLoading: boolean
  profileData: Extract<
    GetProfileQueryInProfileQuery['node'],
    {
      __typename?: 'Profile'
    }
  >
}

βœ… Example: Extracting Only Specific Keys from an Object

type UserKeys = 'id' | 'email'

type UserInfo = Pick<User, UserKeys>

βœ… Example: Omitting Unnecessary Fields from an Object

type User = {
  id: string
  email: string
  password: string
}

type PublicUser = Omit<User, 'password'>

βœ… Example: Combining Multiple Types

Use & to merge multiple types, providing more flexibility than interface extension.

type Base = {
  createdAt: string
}

type Profile = {
  id: string
  name: string
}

type ProfileWithBase = Profile & Base

πŸ”Ή When to Use Extract<> in GraphQL

βœ… Example: Extracting the Profile Type from a Query

type UseGetProfileQueryResult = {
  hasError: ApolloError
  isLoading: boolean
  profileData: Extract<
    GetProfileQueryInProfileQuery['node'],
    {
      __typename?: 'Profile'
    }
  >
}

πŸ”Ή Avoid Unnecessary interface Usage

❌ Bad Example: Using interface for Utility Types

interface UseGetProfileQueryResult {
  hasError: ApolloError
  isLoading: boolean
  profileData: Profile
}

βœ… Good Example: Using type for Flexibility

type UseGetProfileQueryResult = {
  hasError: ApolloError
  isLoading: boolean
  profileData: Profile
}

βœ… Summary


πŸ“ Comments & Documentation

A minimalist approach to comments ensures code is clean, readable, and self-explanatory. Instead of excessive commenting, we prioritize descriptive function and variable names. Comments are used only when necessary, such as for complex logic, workarounds, or TODOs.


πŸ”Ή General Rules


πŸ”Ή Using JSDoc for TODOs

We only use JSDoc (/** @todo */) for tracking future work.

βœ… Example: JSDoc TODO for Future Enhancements

/** @todo Update this when the new API version is available */
const getUserPreferences = async (userId: string) => {
  try {
    return await fetch(`/api/preferences/${userId}`)
  } catch (error) {
    console.error(error)
    return null
  }
}

❌ Avoid Unnecessary TODO Comments

This format is not compatible with JSDoc linters.

// @todo Update this when the new API version is available
const getUserPreferences = async (userId: string) => {
  try {
    return await fetch(`/api/preferences/${userId}`)
  } catch (error) {
    console.error(error)
    return null
  }
}

πŸ’‘ Key Difference:

JSDoc is more structured and aligns with tools that scan TODOs.


πŸ”Ή Inline Comments for Workarounds

Use inline // comments for technical workarounds, browser quirks, or unexpected API behavior.

βœ… Example: Workaround for Safari Quirk

const scrollToTop = () => {
  window.scrollTo(0, 0)
  // Safari requires a slight delay for smooth scrolling
  setTimeout(() => window.scrollTo(0, 0), 10)
}

βœ… Example: Workaround for Safari Quirk with @see

/**
 * Safari requires a slight delay for smooth scrolling.
 * @see https://stackoverflow.com/q/xxxx
 */
const scrollToTop = () => {
  window.scrollTo(0, 0)
  setTimeout(() => window.scrollTo(0, 0), 10)
}

❌ Avoid Redundant Comments

const scrollToTop = () => {
  // Scrolls to the top of the page
  window.scrollTo(0, 0)
}

πŸ’‘ Key Difference:


πŸ”Ή Handling Complex useEffect Hooks

For useEffect, prefer extracting logic into functions instead of writing comments inline.

βœ… Example: Extracting Logic Into a Function

useEffect(() => {
  syncUserPreferences()
}, [])

const syncUserPreferences = async () => {
  try {
    /** @todo Remove this workaround when the API provides real-time updates */
    const preferences = await getUserPreferences(user.id)
    applyUserPreferences(preferences)
  } catch (error) {
    console.error(error)
  }
}

❌ Example of an Overloaded useEffect with Comments

useEffect(() => {
  // Fetch user preferences and apply them
  fetch(`/api/preferences/${user.id}`)
    .then(res => res.json())
    .then(preferences => {
      // Apply user preferences
      applyUserPreferences(preferences)
    })
}, [])

πŸ’‘ Key Takeaway:

πŸ”Ή When to Write a Comment vs. Refactor?

Before writing a comment, ask:


βœ… Summary


🀝 Contributing

Thank you for considering contributing to this project! We appreciate your help in improving and maintaining this repository.


πŸ”Ή How to Contribute

  1. Fork the Repository

    • Click the Fork button on the top right of this repository.
    • This will create a copy under your GitHub account.
  2. Clone Your Fork

    • Run the following command to clone the forked repository:

      git clone https://github.com/YOUR-USERNAME/react-typescript-style-guide.git
      cd react-typescript-style-guide
  3. Make your changes in main

    • Open the project in your preferred editor.
    • Make your changes while following the project’s coding guidelines.
  4. Commit your changes

    git add .
    git commit -m "Describe your changes"
  5. Push to your fork

    git push origin main
  6. Create a Pull Request

    • Go to the original repository on GitHub.
    • Click Compare & pull request.
    • Add a clear description of your changes.
    • Click Create pull request.

πŸ”Ή Contribution Guidelines


βœ… Thank you for contributing! We appreciate your support in improving this project. πŸš€


πŸ“œ License

This project is licensed under the MIT License.

You are free to use, modify, distribute, and share this project with no restrictions, as long as the original license and copyright notice are included.

πŸ“„ Full License

The full license text is available in the LICENSE.md file.


πŸ“š References & Inspirations

This style guide follows widely accepted industry standards while maintaining a minimal, structured, and opinionated approach. Below are key resources that align with and support the philosophy, structure, and best practices outlined in this guide.

πŸ“Œ Key Influences on This Guide

Each of the following references shares core principles with this style guide, such as clarity, maintainability, predictability, and reducing complexity.

ReferenceLinkHow It Relates
Google TypeScript Style GuideGoogle TypeScript Guideβœ… Readability & maintainability via consistent naming, structured function ordering, and predictable patterns.
βœ… Aligns with Component Order and Separation of Concerns principles.
Airbnb React/JSX Style GuideAirbnb React Guideβœ… Focuses on self-contained components, logical function ordering, and clean JSX formatting.
βœ… Strongly aligns with Component Structureβ€”especially hooks, variables, and function organization.
Shopify JavaScript & TypeScript GuideShopify JavaScript & TypeScript Guideβœ… Encourages feature-based folder structure, aligning with Folder Structure.
βœ… Supports encapsulating GraphQL queries within feature folders, similar to our GraphQL Queries section.
TS.dev TypeScript Style GuideTS.dev Guideβœ… Emphasizes clarity and minimalism, reinforcing No Unnecessary Abstraction.
βœ… Aligns with using interfaces for components and types for utilities/hooks.
TypeScript Deep Dive Style GuideTypeScript Deep Diveβœ… Advocates predictable, structured code organization and explicit return types.
βœ… Aligns with Types & Interfaces, particularly Extract<>, Pick<>, and Omit<> usage.

πŸ’‘ Final Thoughts

This style guide follows industry best practices while taking a minimalist approach to ensure scalability, predictability, and maintainability.

By adopting these conventions, you ensure consistency across projects while writing modern, well-structured React + TypeScript code.

πŸš€ Thank you for following this guide! Your contributions help keep codebases clean, readable, and scalable.